diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 411318b..0000000 --- a/.flake8 +++ /dev/null @@ -1,17 +0,0 @@ -[flake8] -doctests = True -select = B,C,E,F,W,T4,B9 -ignore = - # whitespace before ':' - E203, - # line too long - E501, - # missing whitespace around arithmetic operator - E226, - # multiple statements on one line (def) - E704, - # line break before binary operator - W503, -exclude = - .tox, - .venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 296377f..1e7dfa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,13 @@ repos: - repo: local hooks: - - id: pyupgrade - name: pyupgrade - entry: pyupgrade --exit-zero-even-if-changed + - id: ruff + name: ruff + entry: ruff check + language: system + types: [python] + - id: ruff-format + name: ruff-format + entry: ruff format language: system types: [python] diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..2ba1e94 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,25 @@ +target-version = "py39" +exclude = [ + ".eggs", + ".tox", + ".venv", + ".cache", +] + +[lint] +extend-select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warnings +] +ignore = [ + "E501", # line too long +] + +[lint.isort] +known-first-party = ["pgtoolkit"] + +[format] +docstring-code-format = true diff --git a/MANIFEST.in b/MANIFEST.in index d3d8d1b..165ac17 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include .coveragerc include .flake8 include .pre-commit-config.yaml +include .ruff.toml include pyproject.toml include *.md include *.txt diff --git a/pgtoolkit/conf.py b/pgtoolkit/conf.py index c72ad7d..a2648b2 100644 --- a/pgtoolkit/conf.py +++ b/pgtoolkit/conf.py @@ -286,7 +286,7 @@ def serialize_value(value: Value) -> str: # done everywhere in the string or nowhere. if "''" not in value and r"\'" not in value: value = value.replace("'", "''") - value = "'%s'" % value + value = f"'{value}'" elif isinstance(value, timedelta): seconds = value.days * _day + value.seconds if value.microseconds: @@ -340,7 +340,7 @@ def serialize(self) -> str: return serialize_value(self.value) def __str__(self) -> str: - line = "%(name)s = %(value)s" % dict(name=self.name, value=self.serialize()) + line = "{name} = {value}".format(**dict(name=self.name, value=self.serialize())) if self.comment: line += " # " + self.comment if self.commented: @@ -351,33 +351,34 @@ def __str__(self) -> str: class EntriesProxy(dict[str, Entry]): """Proxy object used during Configuration edition. - >>> p = EntriesProxy(port=Entry('port', '5432'), - ... shared_buffers=Entry('shared_buffers', '1GB')) + >>> p = EntriesProxy( + ... port=Entry("port", "5432"), shared_buffers=Entry("shared_buffers", "1GB") + ... ) Existing entries can be edited: - >>> p['port'].value = '5433' + >>> p["port"].value = "5433" New entries can be added as: - >>> p.add('listen_addresses', '*', commented=True, comment='IP address') + >>> p.add("listen_addresses", "*", commented=True, comment="IP address") >>> p # doctest: +NORMALIZE_WHITESPACE {'port': Entry(name='port', _value=5433, commented=False, comment=None), 'shared_buffers': Entry(name='shared_buffers', _value='1GB', commented=False, comment=None), 'listen_addresses': Entry(name='listen_addresses', _value='*', commented=True, comment='IP address')} - >>> del p['shared_buffers'] + >>> del p["shared_buffers"] >>> p # doctest: +NORMALIZE_WHITESPACE {'port': Entry(name='port', _value=5433, commented=False, comment=None), 'listen_addresses': Entry(name='listen_addresses', _value='*', commented=True, comment='IP address')} Adding an existing entry fails: - >>> p.add('port', 5433) + >>> p.add("port", 5433) Traceback (most recent call last): ... ValueError: 'port' key already present So does adding a value to the underlying dict: - >>> p['bonjour_name'] = 'pgserver' + >>> p["bonjour_name"] = "pgserver" Traceback (most recent call last): ... TypeError: cannot set a key @@ -407,16 +408,16 @@ class Configuration: You can access parameter using attribute or dictionary syntax. - >>> conf = parse(['port=5432\n', 'pg_stat_statement.min_duration = 3s\n']) + >>> conf = parse(["port=5432\n", "pg_stat_statement.min_duration = 3s\n"]) >>> conf.port 5432 >>> conf.port = 5433 >>> conf.port 5433 - >>> conf['port'] = 5434 + >>> conf["port"] = 5434 >>> conf.port 5434 - >>> conf['pg_stat_statement.min_duration'].total_seconds() + >>> conf["pg_stat_statement.min_duration"].total_seconds() 3.0 >>> conf.get("ssl") >>> conf.get("ssl", False) @@ -496,7 +497,7 @@ def parse(self, fo: Iterable[str]) -> Iterator[tuple[pathlib.Path, IncludeType]] else: m = self._parameter_re.match(line) if not m: - raise ValueError("Bad line: %r." % raw_line) + raise ValueError(f"Bad line: {raw_line!r}.") kwargs = m.groupdict() name = kwargs.pop("name") value = parse_value(kwargs.pop("value")) @@ -621,12 +622,14 @@ def edit(self) -> Iterator[EntriesProxy]: >>> import sys >>> cfg = Configuration() - >>> includes = cfg.parse([ - ... "#listen_addresses = 'localhost' # what IP address(es) to listen on;\n", - ... " # comma-separated list of addresses;\n", - ... "port = 5432 # (change requires restart)\n", - ... "max_connections = 100 # (change requires restart)\n", - ... ]) + >>> includes = cfg.parse( + ... [ + ... "#listen_addresses = 'localhost' # what IP address(es) to listen on;\n", + ... " # comma-separated list of addresses;\n", + ... "port = 5432 # (change requires restart)\n", + ... "max_connections = 100 # (change requires restart)\n", + ... ] + ... ) >>> list(includes) [] >>> cfg.save(sys.stdout) @@ -638,7 +641,7 @@ def edit(self) -> Iterator[EntriesProxy]: >>> with cfg.edit() as entries: ... entries["port"].value = 2345 ... entries["port"].comment = None - ... entries["listen_addresses"].value = '*' + ... entries["listen_addresses"].value = "*" ... del entries["max_connections"] ... entries.add( ... "unix_socket_directories", diff --git a/pgtoolkit/ctl.py b/pgtoolkit/ctl.py index 3198bb7..f683695 100644 --- a/pgtoolkit/ctl.py +++ b/pgtoolkit/ctl.py @@ -518,13 +518,13 @@ def parse_control_data(lines: Sequence[str]) -> dict[str, str]: def num_version(text_version: str) -> int: """Return PostgreSQL numeric version as defined by LibPQ PQserverVersion - >>> num_version('pg_ctl (PostgreSQL) 9.6.3') + >>> num_version("pg_ctl (PostgreSQL) 9.6.3") 90603 - >>> num_version('pg_ctl (PostgreSQL) 9.2.0') + >>> num_version("pg_ctl (PostgreSQL) 9.2.0") 90200 - >>> num_version('pg_ctl (PostgreSQL) 11.10') + >>> num_version("pg_ctl (PostgreSQL) 11.10") 110010 - >>> num_version('pg_ctl (PostgreSQL) 11.1') + >>> num_version("pg_ctl (PostgreSQL) 11.1") 110001 >>> num_version("pg_ctl (PostgreSQL) 14devel") 140000 diff --git a/pgtoolkit/errors.py b/pgtoolkit/errors.py index 60afdd7..68f151a 100644 --- a/pgtoolkit/errors.py +++ b/pgtoolkit/errors.py @@ -8,15 +8,7 @@ def __init__(self, lineno: int, line: str, message: str) -> None: self.line = line def __repr__(self) -> str: - return "<%s at line %d: %.32s>" % ( - self.__class__.__name__, - self.lineno, - self.args[0], - ) + return f"<{self.__class__.__name__} at line {self.lineno}: {self.args[0]:.32}>" def __str__(self) -> str: - return "Bad line #{} '{:.32}': {}".format( - self.lineno, - self.line.strip(), - self.args[0], - ) + return f"Bad line #{self.lineno} '{self.line.strip():.32}': {self.args[0]}" diff --git a/pgtoolkit/hba.py b/pgtoolkit/hba.py index 8c57d19..b23c0b5 100644 --- a/pgtoolkit/hba.py +++ b/pgtoolkit/hba.py @@ -158,7 +158,7 @@ def parse(cls, line: str) -> HBARecord: comment = " ".join(comments[1:]) if values[0] not in cls.CONNECTION_TYPES: - raise ValueError("Unknown connection type '%s'" % values[0]) + raise ValueError(f"Unknown connection type '{values[0]}'") if "local" != values[0]: record_fields.append("address") common_values = [v for v in values if "=" not in v] @@ -220,14 +220,14 @@ def __str__(self) -> str: continue if width: - fmt += "%%(%s)-%ds " % (field, width - 1) + fmt += f"%%({field})-%ds " % (width - 1) else: fmt += f"%({field})s " # Serialize database and user list using property. values = dict(self.__dict__, databases=self.database, users=self.user) line = fmt.rstrip() % values - auth_options = ['%s="%s"' % i for i in self.auth_options] + auth_options = ['{}="{}"'.format(*i) for i in self.auth_options] if auth_options: line += " " + " ".join(auth_options) @@ -290,7 +290,7 @@ def matches(self, **attrs: str) -> bool: # Provided attributes should be comparable to HBARecord attributes for k in attrs.keys(): if k not in self.COMMON_FIELDS + ["database", "user"]: - raise AttributeError("%s is not a valid attribute" % k) + raise AttributeError(f"{k} is not a valid attribute") for k, v in attrs.items(): if getattr(self, k, None) != v: @@ -326,7 +326,7 @@ def __init__(self, entries: Iterable[HBAComment | HBARecord] | None = None) -> N :param entries: A list of HBAComment or HBARecord. Optional. """ if entries and not isinstance(entries, list): - raise ValueError("%s should be a list" % entries) + raise ValueError(f"{entries} should be a list") self.lines = list(entries) if entries is not None else [] self.path = None diff --git a/pgtoolkit/log/parser.py b/pgtoolkit/log/parser.py index aeb3a85..1fafe86 100644 --- a/pgtoolkit/log/parser.py +++ b/pgtoolkit/log/parser.py @@ -118,11 +118,11 @@ def parse_isodatetime(raw: str) -> datetime: int(raw[20:23]) if raw[19] == "." else 0, ) except ValueError: - raise ValueError("%s is not a known date" % raw) + raise ValueError(f"{raw} is not a known date") if raw[-3:] != "UTC": # We need tzdata for that. - raise ValueError("%s not in UTC." % raw) + raise ValueError(f"{raw} not in UTC.") return datetime(*infos) diff --git a/pgtoolkit/pgpass.py b/pgtoolkit/pgpass.py index 47ae1e3..bda6e72 100644 --- a/pgtoolkit/pgpass.py +++ b/pgtoolkit/pgpass.py @@ -244,13 +244,7 @@ def __lt__(self, other: PassComment | PassEntry) -> bool: return NotImplemented def __repr__(self) -> str: - return "<{} {}@{}:{}/{}>".format( - self.__class__.__name__, - self.username, - self.hostname, - self.port, - self.database, - ) + return f"<{self.__class__.__name__} {self.username}@{self.hostname}:{self.port}/{self.database}>" def __str__(self) -> str: return ":".join( @@ -284,7 +278,7 @@ def matches(self, **attrs: int | str) -> bool: expected_attributes = self.__dict__.keys() for k in attrs.keys(): if k not in expected_attributes: - raise AttributeError("%s is not a valid attribute" % k) + raise AttributeError(f"{k} is not a valid attribute") for k, v in attrs.items(): if getattr(self, k) != v: @@ -328,7 +322,7 @@ def __init__( :param entries: A list of PassEntry or PassComment. Optional. """ if entries and not isinstance(entries, list): - raise ValueError("%s should be a list" % entries) + raise ValueError(f"{entries} should be a list") self.lines = entries or [] self.path = path diff --git a/pgtoolkit/service.py b/pgtoolkit/service.py index bb82edb..fd731e3 100644 --- a/pgtoolkit/service.py +++ b/pgtoolkit/service.py @@ -113,14 +113,14 @@ class Service(dict[str, Parameter]): Each parameters can be accessed either as a dictionary entry or as an attributes. - >>> myservice = Service('myservice', {'dbname': 'mydb'}, host='myhost') + >>> myservice = Service("myservice", {"dbname": "mydb"}, host="myhost") >>> myservice.name 'myservice' >>> myservice.dbname 'mydb' - >>> myservice['dbname'] + >>> myservice["dbname"] 'mydb' - >>> myservice.user = 'myuser' + >>> myservice.user = "myuser" >>> list(sorted(myservice.items())) [('dbname', 'mydb'), ('host', 'myhost'), ('user', 'myuser')] @@ -179,7 +179,7 @@ def __init__(self) -> None: ) def __repr__(self) -> str: - return "<%s>" % (self.__class__.__name__) + return f"<{self.__class__.__name__}>" def __getitem__(self, key: str) -> Service: parameters = { diff --git a/pyproject.toml b/pyproject.toml index 00ee007..c507508 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,11 +31,8 @@ dev = [ "pgtoolkit[lint,typing,test,doc]", ] lint = [ - "black", + "ruff", "check-manifest", - "flake8", - "isort", - "pyupgrade", ] typing = [ "mypy", @@ -57,10 +54,6 @@ doc = [ Repository = "https://github.com/dalibo/pgtoolkit" Documentation = "https://pgtoolkit.readthedocs.io/" -[tool.isort] -profile = "black" -multi_line_output = 3 - [tool.setuptools.packages.find] where = ["."] diff --git a/tests/test_conf.py b/tests/test_conf.py index c23d1d1..a0af2ca 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -45,8 +45,7 @@ def test_parse_value(): assert "user=foo password=secret'" == parse_value("'user=foo password=secret'''") assert ( # this one does not work in parse_dsn() - "user=foo password='secret" - == parse_value("'user=foo password=''secret'") + "user=foo password='secret" == parse_value("'user=foo password=''secret'") ) assert "%m [%p] %q%u@%d " == parse_value(r"'%m [%p] %q%u@%d '") assert "124.7MB" == parse_value("124.7MB") diff --git a/tests/test_hba.py b/tests/test_hba.py index b1fbcc1..e90b045 100644 --- a/tests/test_hba.py +++ b/tests/test_hba.py @@ -63,9 +63,7 @@ def test_parse_local_line(): with pytest.raises(AttributeError): record.address - wanted = ( - "local all all trust" # noqa - ) + wanted = "local all all trust" # noqa assert wanted == str(record) diff --git a/tests/test_log.py b/tests/test_log.py index ec0b386..aa6ec19 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -25,9 +25,7 @@ def test_parse(): \tORDER BY 1; 2018-06-15 10:49:26.088 UTC [8420]: [4-1] app=psql,db=postgres,client=[local],user=postgres LOG: disconnection: session time: 0:00:00.006 user=postgres database=postgres host=[local] BAD PREFIX 10:49:31.140 UTC [8423]: [1-1] app=[unknown],db=[unknown],client=[local],user=[unknown] LOG: connection received: host=[local] -""".splitlines( - True - ) # noqa +""".splitlines(True) # noqa log_line_prefix = "%m [%p]: [%l-1] app=%a,db=%d,client=%h,user=%u " records = list(parse(lines, prefix_fmt=log_line_prefix)) @@ -57,9 +55,7 @@ def test_group_lines(): \tORDER BY 1; 2018-06-15 10:49:26.088 UTC [8420]: [4-1] app=psql,db=postgres,client=[local],user=postgres LOG: disconnection: session time: 0:00:00.006 user=postgres database=postgres host=[local] 2018-06-15 10:49:31.140 UTC [8423]: [1-1] app=[unknown],db=[unknown],client=[local],user=[unknown] LOG: connection received: host=[local] -""".splitlines( - True - ) # noqa +""".splitlines(True) # noqa groups = list(group_lines(lines)) assert 7 == len(groups) @@ -128,9 +124,7 @@ def test_record_stage1_ok(): \t pg_catalog.array_to_string(d.datacl, E'\\n') AS "Access privileges" \tFROM pg_catalog.pg_database d \tORDER BY 1; -""".splitlines( - True - ) # noqa +""".splitlines(True) # noqa record = Record.parse_stage1(lines) assert "LOG" in repr(record) @@ -173,9 +167,7 @@ def test_filters(): stage1 LOG: duration: 1002.209 ms statement: select pg_sleep(1); stage2 LOG: duration: 0.223 ms statement: show log_timezone; stage3 LOG: connection authorized: user=postgres database=postgres -""".splitlines( - True - ) # noqa +""".splitlines(True) # noqa class MyFilters(NoopFilters): def stage1(self, record): diff --git a/tox.ini b/tox.ini index 051457b..db7b2dd 100644 --- a/tox.ini +++ b/tox.ini @@ -5,18 +5,12 @@ isolated_build = true [testenv:lint] commands= - flake8 - black --check --diff . - isort --check --diff . - pre-commit run --all-files --show-diff-on-failure pyupgrade + ruff check + ruff format --check check-manifest deps = - flake8 - black check-manifest - isort - pre-commit - pyupgrade + ruff skip_install = true [testenv:tests{,-ci}]