diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3907721..9c87740 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,9 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 + rev: v0.6.7 hooks: + # Run the linter. - id: ruff - args: - - --fix - - repo: https://github.com/psf/black - rev: 23.12.1 - hooks: - - id: black - language_version: python3 - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/georss_ingv_centro_nazionale_terremoti_client/__init__.py b/georss_ingv_centro_nazionale_terremoti_client/__init__.py index 1e31c6e..4b81f83 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/__init__.py +++ b/georss_ingv_centro_nazionale_terremoti_client/__init__.py @@ -1,3 +1,4 @@ """INGV Centro Nazionale Terremoti (Earthquakes) library.""" + from .feed import IngvCentroNazionaleTerremotiFeed # noqa: F401 from .feed_manager import IngvCentroNazionaleTerremotiFeedManager # noqa: F401 diff --git a/georss_ingv_centro_nazionale_terremoti_client/__version__.py b/georss_ingv_centro_nazionale_terremoti_client/__version__.py index e3a0ca4..83f8283 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/__version__.py +++ b/georss_ingv_centro_nazionale_terremoti_client/__version__.py @@ -1,2 +1,3 @@ """Define a version constant.""" + __version__ = "0.7" diff --git a/georss_ingv_centro_nazionale_terremoti_client/consts.py b/georss_ingv_centro_nazionale_terremoti_client/consts.py index fa35907..5c01031 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/consts.py +++ b/georss_ingv_centro_nazionale_terremoti_client/consts.py @@ -1,4 +1,5 @@ """INGV Centro Nazionale Terremoti (Earthquakes) consts.""" + from georss_client import CUSTOM_ATTRIBUTE IMAGE_URL_PATTERN = ( @@ -6,8 +7,8 @@ ) REGEXP_ATTR_MAGNITUDE = rf"Magnitude\(M.{{0,3}}\) (?P<{CUSTOM_ATTRIBUTE}>[^ ]+) " -REGEXP_ATTR_REGION = r"Magnitude\(M.{{0,3}}\) [^ ]+[ ]+-[ ]+(?P<{}>.+)$".format( - CUSTOM_ATTRIBUTE +REGEXP_ATTR_REGION = ( + rf"Magnitude\(M.{{0,3}}\) [^ ]+[ ]+-[ ]+(?P<{CUSTOM_ATTRIBUTE}>.+)$" ) REGEXP_ATTR_EVENT_ID = rf"eventId=(?P<{CUSTOM_ATTRIBUTE}>\d+)$" diff --git a/georss_ingv_centro_nazionale_terremoti_client/feed.py b/georss_ingv_centro_nazionale_terremoti_client/feed.py index 29a8159..341617d 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/feed.py +++ b/georss_ingv_centro_nazionale_terremoti_client/feed.py @@ -1,4 +1,5 @@ """INGV Centro Nazionale Terremoti (Earthquakes) feed.""" + from __future__ import annotations from georss_client import ATTR_ATTRIBUTION, GeoRssFeed @@ -13,8 +14,8 @@ class IngvCentroNazionaleTerremotiFeed(GeoRssFeed): def __init__( self, home_coordinates: tuple[float, float], - filter_radius: float = None, - filter_minimum_magnitude: float = None, + filter_radius: float | None = None, + filter_minimum_magnitude: float | None = None, ): """Initialise this service.""" super().__init__(home_coordinates, URL, filter_radius=filter_radius) @@ -22,13 +23,7 @@ def __init__( def __repr__(self): """Return string representation of this feed.""" - return "<{}(home={}, url={}, radius={}, magnitude={})>".format( - self.__class__.__name__, - self._home_coordinates, - self._url, - self._filter_radius, - self._filter_minimum_magnitude, - ) + return f"<{self.__class__.__name__}(home={self._home_coordinates}, url={self._url}, radius={self._filter_radius}, magnitude={self._filter_minimum_magnitude})>" def _new_entry(self, home_coordinates, rss_entry, global_data): """Generate a new entry.""" diff --git a/georss_ingv_centro_nazionale_terremoti_client/feed_entry.py b/georss_ingv_centro_nazionale_terremoti_client/feed_entry.py index fba830d..1f465c5 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/feed_entry.py +++ b/georss_ingv_centro_nazionale_terremoti_client/feed_entry.py @@ -1,4 +1,5 @@ """INGV Centro Nazionale Terremoti (Earthquakes) feed entry.""" + from __future__ import annotations from georss_client import FeedEntry diff --git a/georss_ingv_centro_nazionale_terremoti_client/feed_manager.py b/georss_ingv_centro_nazionale_terremoti_client/feed_manager.py index b765ef5..5024dba 100644 --- a/georss_ingv_centro_nazionale_terremoti_client/feed_manager.py +++ b/georss_ingv_centro_nazionale_terremoti_client/feed_manager.py @@ -1,4 +1,5 @@ """INGV Centro Nazionale Terremoti (Earthquakes) feed manager.""" + from __future__ import annotations from georss_client.feed_manager import FeedManagerBase @@ -15,8 +16,8 @@ def __init__( update_callback, remove_callback, coordinates: tuple[float, float], - filter_radius: float = None, - filter_minimum_magnitude: float = None, + filter_radius: float | None = None, + filter_minimum_magnitude: float | None = None, ): """Initialize the INGV Centro Nazionale Terremoti Feed Manager.""" feed = IngvCentroNazionaleTerremotiFeed( diff --git a/pyproject.toml b/pyproject.toml index e7f82aa..70b89a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,48 +1,135 @@ -[tool.black] -target-version = ['py39', 'py310', 'py311', 'py312'] - -[tool.isort] -profile = "black" -src_paths = ["georss_ingv_centro_nazionale_terremoti_client", "tests"] - -[tool.ruff] -target-version = "py310" - +[tool.ruff.lint] select = [ + "A001", # Variable {name} is shadowing a Python builtin + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception - "C", # complexity - "D", # docstrings - "E", # pycodestyle - "F", # pyflakes/autoflake + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil + "B018", # Found useless attribute access. Either assign it to a variable or remove it. + "B023", # Function definition does not bind loop variable {name} + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T20", # flake8-print - "TRY004", # Prefer TypeError exception for invalid type + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task - "UP", # pyupgrade - "W", # pycodestyle + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TID251", # Banned imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle ] ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D406", # Section name should end with a newline - "D407", # Section name underlining - "E501", # line too long - "E731", # do not assign a lambda expression, use a def + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT004", # Fixture {fixture} does not return anything, add leading underscore + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT012", # `pytest.raises()` block should contain a single simple statement + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605", +] + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [ + "georss_ingv_centro_nazionale_terremoti_client", ] +combine-as-imports = true +split-on-trailing-comma = false [tool.pytest.ini_options] testpaths = [ diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d21adf9..0000000 --- a/setup.cfg +++ /dev/null @@ -1,15 +0,0 @@ -[flake8] -exclude = .git,.tox,venv,bin,lib,deps,build -doctests = True -# To work with Black -# E501: line too long -# W503: Line break occurred before a binary operator -# E203: Whitespace before ':' -# D202 No blank lines allowed after function docstring -# W504 line break after binary operator -ignore = - E501, - W503, - E203, - D202, - W504 diff --git a/setup.py b/setup.py index 476d2a3..7acde23 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ """Setup of georss_ingv_centro_nazionale_terremoti_client library.""" -import os from setuptools import find_packages, setup +from georss_ingv_centro_nazionale_terremoti_client.__version__ import __version__ + NAME = "georss_ingv_centro_nazionale_terremoti_client" AUTHOR = "Malte Franken" AUTHOR_EMAIL = "coding@subspace.de" @@ -19,14 +20,9 @@ with open("README.md") as fh: long_description = fh.read() -HERE = os.path.abspath(os.path.dirname(__file__)) -VERSION = {} -with open(os.path.join(HERE, NAME, "__version__.py")) as f: - exec(f.read(), VERSION) # pylint: disable=exec-used - setup( name=NAME, - version=VERSION["__version__"], + version=__version__, author=AUTHOR, author_email=AUTHOR_EMAIL, description=DESCRIPTION, diff --git a/tests/__init__.py b/tests/__init__.py index 2b48178..c2e391f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,5 @@ """Tests for georss-ingv-centro-nazionale-terremoti-client library.""" + import os diff --git a/tests/test_init.py b/tests/test_init.py index 849651c..9f20ca9 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,4 +1,5 @@ """Test for the INGV Centro Nazionale Terremoti feed.""" + import datetime import unittest from unittest import mock @@ -37,7 +38,7 @@ def test_update_ok(self, mock_session, mock_request): ) status, entries = feed.update() assert status == UPDATE_OK - self.assertIsNotNone(entries) + assert entries is not None assert len(entries) == 3 feed_entry = entries[0] @@ -52,7 +53,7 @@ def test_update_ok(self, mock_session, mock_request): assert feed_entry.event_id == 1234 assert feed_entry.image_url is None assert feed_entry.coordinates == (37.654, 14.878) - self.assertAlmostEqual(feed_entry.distance_to_home, 358.4, 1) + assert round(abs(feed_entry.distance_to_home - 358.4), 1) == 0 assert feed_entry.published == datetime.datetime( 2018, 10, 6, 8, 0, tzinfo=datetime.timezone.utc ) @@ -76,7 +77,7 @@ def test_update_ok(self, mock_session, mock_request): feed_entry.title == "2018-10-06 09:14:11 UTC - Magnitude(ML)" " 0.7 - 1 km NE Norcia (PG)" ) - self.assertIsNone(feed_entry.published) + assert feed_entry.published is None feed_entry = entries[2] assert feed_entry.event_id == 3456 @@ -99,7 +100,7 @@ def test_update_ok_with_category(self, mock_session, mock_request): ) status, entries = feed.update() assert status == UPDATE_OK - self.assertIsNotNone(entries) + assert entries is not None assert len(entries) == 2 feed_entry = entries[0] @@ -152,7 +153,7 @@ def _remove_entity(external_id): ) feed_manager.update() entries = feed_manager.feed_entries - self.assertIsNotNone(entries) + assert entries is not None assert len(entries) == 3 assert feed_manager.last_timestamp == datetime.datetime( 2018, 10, 6, 8, 0, tzinfo=datetime.timezone.utc