diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 6ab1826..7b089e2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -4,6 +4,10 @@ on: - push - pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: publish-python-version: 3.12 @@ -28,6 +32,7 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pr_towncrier.yml b/.github/workflows/pr_towncrier.yml index e195583..d761277 100644 --- a/.github/workflows/pr_towncrier.yml +++ b/.github/workflows/pr_towncrier.yml @@ -13,7 +13,7 @@ jobs: env: PR_NUMBER: ${{ github.event.number }} run: | - if [ ! -f "changes/${PR_NUMBER}.feature" ] && [ ! -f "changes/${PR_NUMBER}.bugfix" ] && [ ! -f "changes/${PR_NUMBER}.doc" ] && [ ! -f "changes/${PR_NUMBER}.removal" ] && [ ! -f "changes/${PR_NUMBER}.misc" ]; then + if [ ! -f "changes/${PR_NUMBER}.feature.rst" ] && [ ! -f "changes/${PR_NUMBER}.bugfix.rst" ] && [ ! -f "changes/${PR_NUMBER}.doc.rst" ] && [ ! -f "changes/${PR_NUMBER}.removal.rst" ] && [ ! -f "changes/${PR_NUMBER}.misc.rst" ]; then echo "Towncrier file for #${PR_NUMBER} not found. Please add a changes file to the `changes/` directory. See README.rst for more information." exit 1 fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6868762..fde0cb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,20 @@ +ci: + autofix_prs: true + autoupdate_schedule: weekly + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v6.0.0' hooks: - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: '25.12.0' + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.15.1' hooks: - - id: black - args: - - --safe - - --quiet + - id: ruff + args: [--fix] + files: ^(didl_lite|tests)/.+\.py$ + - id: ruff-format files: ^(didl_lite|tests)/.+\.py$ - repo: https://github.com/codespell-project/codespell rev: 'v2.4.1' @@ -21,20 +25,6 @@ repos: - --quiet-level=2 exclude_types: [csv, json] exclude: ^tests/fixtures/ - - repo: https://github.com/pycqa/flake8 - rev: '7.3.0' - hooks: - - id: flake8 - additional_dependencies: - - flake8-docstrings==1.7.0 - - pydocstyle==6.3.0 - files: ^(didl_lite|tests)/.+\.py$ - - repo: https://github.com/PyCQA/isort - rev: '7.0.0' - hooks: - - id: isort - args: - - --profile=black - repo: https://github.com/pre-commit/mirrors-mypy rev: 'v1.19.1' hooks: @@ -43,3 +33,7 @@ repos: additional_dependencies: - pytest==9.0.2 files: ^(didl_lite|tests)/.+\.py$ + - repo: https://github.com/fpgmaas/deptry + rev: '0.24.0' + hooks: + - id: deptry diff --git a/changes/39.misc.rst b/changes/39.misc.rst new file mode 100644 index 0000000..8a33934 --- /dev/null +++ b/changes/39.misc.rst @@ -0,0 +1 @@ +Update CI and dependencies diff --git a/didl_lite/__init__.py b/didl_lite/__init__.py index dcf0198..22fa90a 100644 --- a/didl_lite/__init__.py +++ b/didl_lite/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DIDL-Lite (Digital Item Declaration Language) tools for Python.""" from didl_lite import didl_lite # noqa: F401 diff --git a/didl_lite/didl_lite.py b/didl_lite/didl_lite.py index de42326..c9acb87 100644 --- a/didl_lite/didl_lite.py +++ b/didl_lite/didl_lite.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DIDL-Lite (Digital Item Declaration Language) tools for Python.""" # pylint: disable=too-many-lines @@ -101,9 +100,7 @@ def __init__( self.xml_el = xml_el self.descriptors = descriptors if descriptors else [] - def _ensure_required_properties( - self, strict: bool, properties: Mapping[str, Any] - ) -> None: + def _ensure_required_properties(self, strict: bool, properties: Mapping[str, Any]) -> None: """Check if all required properties are given.""" if not strict: return @@ -184,9 +181,7 @@ def to_xml(self) -> ET.Element: continue key = didl_property_def_key(property_def) - if ( - getattr(self, key) is None or key == "res" - ): # no resources, handled later on + if getattr(self, key) is None or key == "res": # no resources, handled later on continue tag = property_def[0] + ":" + property_def[1] @@ -245,11 +240,7 @@ def __setattr__(self, name: str, value: Any) -> None: def __repr__(self) -> str: """Evaluatable string representation of this object.""" class_name = type(self).__name__ - attr = ", ".join( - f"{key}={val!r}" - for key, val in self.__dict__.items() - if key not in ("class", "xml_el") - ) + attr = ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items() if key not in ("class", "xml_el")) return f"{class_name}({attr})" @@ -660,11 +651,7 @@ def to_xml(self) -> ET.Element: def __repr__(self) -> str: """Evaluatable string representation of this object.""" class_name = type(self).__name__ - attr = ", ".join( - f"{key}={val!r}" - for key, val in self.__dict__.items() - if key not in ("class", "xml_el") - ) + attr = ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items() if key not in ("class", "xml_el")) children_repr = ", ".join(repr(child) for child in self) return f"{class_name}({attr}, children=[{children_repr}])" @@ -988,11 +975,7 @@ def to_xml(self) -> ET.Element: def __repr__(self) -> str: """Evaluatable string representation of this object.""" class_name = type(self).__name__ - attr = ", ".join( - f"{key}={val!r}" - for key, val in self.__dict__.items() - if val is not None and key != "xml_el" - ) + attr = ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items() if val is not None and key != "xml_el") return f"{class_name}({attr})" @@ -1045,11 +1028,7 @@ def __getattr__(self, name: str) -> Any: def __repr__(self) -> str: """Evaluatable string representation of this object.""" class_name = type(self).__name__ - attr = ", ".join( - f"{key}={val!r}" - for key, val in self.__dict__.items() - if val is not None and key != "xml_el" - ) + attr = ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items() if val is not None and key != "xml_el") return f"{class_name}({attr})" @@ -1071,9 +1050,7 @@ def to_xml_string(*objects: DidlObject) -> bytes: return ET.tostring(root_el) -def from_xml_string( - xml_string: str, strict: bool = True -) -> List[Union[DidlObject, Descriptor]]: +def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObject, Descriptor]]: """Parse DIDL-Lite XML string.""" if not strict: # Find all prefixes used in tags, e.g., @@ -1083,9 +1060,7 @@ def from_xml_string( defined_prefixes = set(re.findall(r"xmlns:([a-zA-Z0-9]+)=", xml_string)) # Identify prefixes used but not defined. - missing_prefixes = ( - used_prefixes - defined_prefixes - {"DIDL-Lite", "dc", "upnp", "dlna"} - ) + missing_prefixes = used_prefixes - defined_prefixes - {"DIDL-Lite", "dc", "upnp", "dlna"} # Remove the "if missing_prefixes:" line and just keep the for loop for prefix in missing_prefixes: @@ -1099,17 +1074,15 @@ def from_xml_string( return from_xml_el(xml_el, strict) -def from_xml_el( - xml_el: ET.Element, strict: bool = True -) -> List[Union[DidlObject, Descriptor]]: +def from_xml_el(xml_el: ET.Element, strict: bool = True) -> List[Union[DidlObject, Descriptor]]: """Convert XML Element to DIDL Objects.""" didl_objects = [] # type: List[Union[DidlObject, Descriptor]] # items and containers, in order for child_el in xml_el: - if child_el.tag != expand_namespace_tag( - "didl_lite:item" - ) and child_el.tag != expand_namespace_tag("didl_lite:container"): + if child_el.tag != expand_namespace_tag("didl_lite:item") and child_el.tag != expand_namespace_tag( + "didl_lite:container" + ): continue # construct item @@ -1139,9 +1112,7 @@ def from_xml_el( # upnp_class to python type mapping -def type_by_upnp_class( - upnp_class: str, strict: bool = True -) -> Optional[Type[DidlObject]]: +def type_by_upnp_class(upnp_class: str, strict: bool = True) -> Optional[Type[DidlObject]]: """Get DidlObject-type by upnp_class. When strict is False, the upnp_class lookup will be done ignoring string diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 360b504..0000000 --- a/pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[BASIC] -good-names=otherItem, storageMedium diff --git a/pyproject.toml b/pyproject.toml index 77e6d14..fa3d387 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,9 +46,6 @@ version = { attr = "didl_lite.__version__" } [tool.setuptools.package-data] didl_lite = ["py.typed"] -[tool.flake8] -max-line-length = 99 - [tool.mypy] check_untyped_defs = true disallow_untyped_calls = true @@ -61,3 +58,37 @@ warn_redundant_casts = true warn_return_any = true warn_unused_configs = true warn_unused_ignores = true + +[tool.ruff] +line-length = 119 + +[tool.ruff.lint] +select = ["C901", "D", "E", "F", "I", "W"] +ignore = ["D202"] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.isort] +known-first-party = ["didl_lite"] + +[tool.deptry] +package_module_name_map = { "pytest" = "pytest" } + +[tool.deptry.per_rule_ignores] +DEP002 = ["pytest"] + +[tool.pylint.format] +max-line-length = 119 + +[tool.pylint.basic] +good-names = ["otherItem", "storageMedium"] + +[tool.codespell] +ignore-words-list = "wan" + +[tool.coverage.run] +source = ["didl_lite"] diff --git a/setup.cfg b/setup.cfg index d045450..8bc5937 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,40 +7,3 @@ tag_name = {new_version} [bumpversion:file:didl_lite/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" - -[bdist_wheel] -python-tag = py3 - -[metadata] -license_file = LICENSE.md - -[flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build -max-line-length = 119 -max-complexity = 25 -ignore = - E501, - W503, - E203, - D202, - W504 -noqa-require-code = True - -[mypy] -check_untyped_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_return_any = true -warn_unused_configs = true -warn_unused_ignores = true - -[codespell] -ignore-words-list = wan - -[coverage:run] -source = didl_lite diff --git a/tests/test_didl_lite.py b/tests/test_didl_lite.py index e4627b0..c60cf07 100644 --- a/tests/test_didl_lite.py +++ b/tests/test_didl_lite.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Unit tests for didl_lite.""" import pytest @@ -234,9 +233,7 @@ def test_container_from_xml(self) -> None: def test_container_to_xml(self) -> None: """Test container to XML.""" - container = didl_lite.Album( - id="0", parent_id="0", title="Audio Item Title", restricted="1" - ) + container = didl_lite.Album(id="0", parent_id="0", title="Audio Item Title", restricted="1") resource = didl_lite.Resource("url", "protocol_info") item = didl_lite.AudioItem( id="0", @@ -285,9 +282,7 @@ def test_container_repr(self) -> None: # pylint: disable=import-outside-toplevel from didl_lite.didl_lite import Album, AudioItem, Resource - container = Album( - id="0", parent_id="0", title="Audio Item Title", restricted="1" - ) + container = Album(id="0", parent_id="0", title="Audio Item Title", restricted="1") resource = Resource("url", "protocol_info") item = AudioItem( id="0", @@ -427,9 +422,7 @@ def test_descriptor_from_xml_container_item(self) -> None: def test_descriptor_to_xml(self) -> None: """Test descriptor to XML.""" - descriptor = didl_lite.Descriptor( - id="1", name_space="ns", type="type", text="Text" - ) + descriptor = didl_lite.Descriptor(id="1", name_space="ns", type="type", text="Text") item = didl_lite.AudioItem( id="0", parent_id="0", @@ -468,9 +461,7 @@ def test_descriptor_repr(self) -> None: descriptor_repr = repr(descriptor) descriptor_remade = eval(descriptor_repr) # pylint: disable=eval-used - assert ET.tostring(descriptor.to_xml()) == ET.tostring( - descriptor_remade.to_xml() - ) + assert ET.tostring(descriptor.to_xml()) == ET.tostring(descriptor_remade.to_xml()) def test_item_order(self) -> None: """Test item ordering.""" @@ -608,9 +599,7 @@ def test_extra_properties(self) -> None: def test_default_properties_set(self) -> None: """Test defaults for item properties.""" - item = didl_lite.VideoItem( - id="0", parent_id="0", title="Video Item Title", restricted="1" - ) + item = didl_lite.VideoItem(id="0", parent_id="0", title="Video Item Title", restricted="1") assert hasattr(item, "genre_type") # property is set def test_property_case(self) -> None: diff --git a/tox.ini b/tox.ini index 3682909..dee8e68 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py310, py311, py312, py313, py314, flake8, pylint, codespell, mypy, black, isort +envlist = py310, py311, py312, py313, py314, ruff, pylint, codespell, mypy, deptry [gh-actions] python = 3.10: py310 3.11: py311 - 3.12: py312, flake8, pylint, codespell, mypy, black, isort + 3.12: py312, ruff, pylint, codespell, mypy, deptry 3.13: py313 3.14: py314 @@ -16,17 +16,16 @@ ignore_errors = True deps = pytest == 9.0.2 pytest-cov == 7.0.0 - coverage == 7.13.1 + coverage == 7.13.4 -[testenv:flake8] +[testenv:ruff] basepython = python3 ignore_errors = True deps = - flake8 == 7.3.0 - flake8-docstrings == 1.7.0 - flake8-noqa == 1.4.0 - pydocstyle == 6.3.0 -commands = flake8 didl_lite tests + ruff == 0.15.1 +commands = + ruff check didl_lite tests + ruff format --check didl_lite tests [testenv:pylint] basepython = python3 @@ -51,15 +50,9 @@ deps = pytest == 9.0.2 commands = mypy --ignore-missing-imports didl_lite tests -[testenv:black] -basepython = python3 -deps = - black == 25.12.0 -commands = black --diff didl_lite tests - -[testenv:isort] +[testenv:deptry] basepython = python3 ignore_errors = True deps = - isort == 7.0.0 -commands = isort --check-only --diff --profile=black didl_lite tests + deptry == 0.24.0 +commands = deptry .