Skip to content

build: pyproject migration, shared tooling (ruff/pylint/uv), lint cleanup & typing - #21

Open
vidiecan wants to merge 11 commits into
dtqfrom
feat/pyproject-tooling-typing
Open

vidiecan wants to merge 11 commits into
dtqfrom
feat/pyproject-tooling-typing

Conversation

@vidiecan

@vidiecan vidiecan commented Aug 17, 2026 •

Copy link
Copy Markdown

Makes dspace_rest_client a first-class project aligned with the consumer repo's conventions (AGENTS.md, .pre-commit-config.yaml, pyproject.toml). Five threads, all behaviour-preserving — all 68 tests still pass.

What changed

1. Packaging → PEP 621 pyproject.toml (replaces setup.py)

  • Static metadata, requests as the only core dep, solr + dev optional-dependency groups.
  • Type hints live in the source (.py), which is how this repo consumes the library (submodule on sys.path); no py.typed/MANIFEST.in ceremony is carried for a package that is never pip-installed here.
  • Dropped requirements.txt — fpdf/textblob/setuptools were unused cruft; runtime deps now live in pyproject. requirements-test.txt is kept (the consumer repo's CI installs it).

2. Shared toolchain (.pre-commit-config.yaml + [tool.*])

  • Mirrors the consumer repo: mixed-line-ending, autopep8 --max-line-length=90, ruff, pylint --rcfile=pyproject.toml.
  • [tool.ruff] / [tool.pylint] / [tool.autopep8] / [tool.mypy] copied and adapted from the consumer standard.

3. uv / uvx dev + CI workflow

  • CI now runs ruff, pylint and mypy via uvx (no global installs); local dev via uvx/uv run.

4. CI jobs (.github/workflows/tests.yml)

  • Added a lint job (ruff 0.12.7 + pylint 3.3.7, versions pinned to the consumer's) and a typecheck job (mypy, continue-on-error — see typing note). Existing pytest matrix (3.10 / 3.12) retained; pip cache re-keyed off pyproject.toml.

5. Lint cleanup + type hints — pylint 9.36 → 10.00 / 10, no behaviour change:

  • wildcard imports → explicit imports + __all__ (client, __init__);
  • extracted _is_valid_uuid(), stopped shadowing the builtin id;
  • fixed a dangerous mutable default arg (proxies), list()/dict() literals, no-else-return, consider-using-in, inconsistent-return-statements, logging-not-lazy, pointless-string-statement, unused vars;
  • collapsed a redundant WorkspaceItem override to inheritance;
  • added type hints (from __future__ import annotations) across models.py / client.py.

Validation (local, CI-exact versions)

Check Result
pytest 68 passed
ruff 0.12.7 (dspace_rest_client + tests) clean
pylint 3.3.7 (--rcfile=pyproject.toml) 10.00 / 10
wheel build (uv build) OK

Notes for the reviewer

  • Supported Python is now >=3.10 (was >=3.8 in the old setup.py), and the classifiers were trimmed to match. This is intentional, not incidental: CI only tests 3.10/3.12, the consumer repo targets 3.10+, and upstream has moved to 3.13. It does not come from a runtime requirement — from __future__ import annotations keeps the new PEP 604 (X | None) hints lazy — it simply makes the metadata reflect what we actually test and support.
  • Typing is phased/non-blocking on purpose. The typecheck job is continue-on-error: true. Baseline: ~54 mypy (default mode) / 289 --strict errors, to be burned down incrementally; the remaining ones are pre-existing dynamic patterns (e.g. params = None reassignment, optional self.solr), not regressions from the added hints.
  • Deliberately deferred (config-suppressed with justification in [tool.pylint], tracked as follow-ups, not done here to keep the diff low-risk): splitting the 1482-line DSpaceClient god-class (too-many-lines / too-many-public-methods), and the mutable class-attribute defaults on the model classes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the dspace_rest_client library by migrating packaging/configuration to pyproject.toml, aligning local tooling with shared repo standards (pre-commit, ruff, pylint, mypy), and updating CI to run lint + typecheck jobs alongside the existing pytest matrix. It also includes largely behavior-preserving lint cleanups and type-hint additions across the client/models.

Changes:

  • Migrates packaging from setup.py/requirements.txt to PEP 621 pyproject.toml, adds MANIFEST.in + py.typed marker.
  • Adds shared developer tooling (.pre-commit-config.yaml, ruff/pylint/mypy config in pyproject.toml) and CI lint/typecheck jobs using uvx.
  • Performs lint-driven refactors and typing improvements in dspace_rest_client/client.py, dspace_rest_client/models.py, and package exports in dspace_rest_client/__init__.py.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
setup.py Removed legacy setuptools build entrypoint in favor of pyproject.toml.
requirements.txt Removed unused/legacy dependency list now represented in pyproject.toml.
requirements-test.txt Updates CI/install commentary to reflect pyproject-based installs.
pyproject.toml Adds PEP 621 metadata plus ruff/pylint/mypy/autopep8 configuration.
MANIFEST.in Ensures README.md and py.typed are included in source distributions.
dspace_rest_client/py.typed Marks the package as typed (PEP 561).
dspace_rest_client/models.py Adds annotations and lint cleanups; simplifies redundant subclass override.
dspace_rest_client/client.py Removes wildcard imports, adds typing, small refactors, and safer defaults.
dspace_rest_client/init.py Replaces wildcard re-export with explicit public API exports.
.pre-commit-config.yaml Adds pre-commit hooks for formatting/linting consistent with the consumer repo.
.github/workflows/tests.yml Keeps pytest matrix and adds lint + non-blocking typecheck jobs via uvx.
Suppressed comments (2)

dspace_rest_client/models.py:67

  • This is a mutable class attribute ({}), which is shared across instances if any code mutates it before __init__ assigns an instance attribute. Prefer an annotation-only attribute (or None) and initialize per-instance in __init__ (which you already do).
    metadata = {}

dspace_rest_client/models.py:246

  • This is a mutable class attribute ({}), which is shared across instances. Since the base initializer sets self.metadata, this can be an annotation-only attribute to avoid an unnecessary shared default at the class level.
    metadata = {}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyproject.toml Outdated
Comment thread dspace_rest_client/models.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

pyproject.toml:10

  • Because this README is also the published package description, its installation requirements must be updated with the migration. It still advertises Python 3.x/3.8 and tells source users to install the now-deleted requirements.txt (README.md:13-26), so the documented install command fails and the Python requirement conflicts with >=3.10.
readme = "README.md"
requires-python = ">=3.10"

dspace_rest_client/client.py:561

  • The new public annotation rejects a supported and tested call: this method deliberately treats action=None as “omit the action filter” (see tests/test_client_read.py:283-297 and the guard below), but the str annotation makes typed consumers fail that call now that the package ships py.typed. Include None in the parameter type.
    def get_resourcepolicy(self, uuid: str, action: str = 'READ') -> Optional[list]:

dspace_rest_client/client.py:141

  • The mutable-default problem remains because every client constructed without proxies receives the same class-level dictionary. Mutating one instance's client.proxies therefore changes the proxy configuration of other default instances; copy the default when assigning it.
        self.proxies = proxies if proxies is not None else self.PROXY_DICT

Comment thread pyproject.toml
Comment on lines +1 to +3
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
jm added 11 commits October 7, 2026 09:18
Replace setup.py with a PEP 621 pyproject.toml (static metadata, requests
core dep, solr + dev optional-dependency groups) and mirror the consumer
repo's toolchain so both share one standard:
- .pre-commit-config.yaml (mixed-line-ending, autopep8, ruff, pylint)
- [tool.ruff] / [tool.pylint] / [tool.autopep8] / [tool.mypy] config
- ship a py.typed marker (PEP 561) via package-data + MANIFEST.in

Drop requirements.txt (fpdf/textblob/setuptools were unused cruft; runtime
deps are now declared in pyproject). requirements-test.txt is kept because
the consumer repo's CI installs it.
Add a ruff+pylint lint job and a mypy typecheck job (continue-on-error,
since strict typing is being introduced incrementally) alongside the
existing pytest matrix, all run via uvx. Key the pip cache off
pyproject.toml now that setup.py is gone.
Bring dspace_rest_client to a clean 10.00/10 under the shared pylint
config without changing behavior (all 68 tests still pass):
- replace wildcard imports with explicit imports + __all__ (client, __init__)
- extract a _is_valid_uuid() helper; stop shadowing the builtin id
- fix a dangerous mutable default arg (proxies), list()/dict() literals,
  no-else-return, consider-using-in, inconsistent-return-statements,
  logging-not-lazy, pointless-string-statement and unused variables
- collapse a redundant WorkspaceItem override to plain inheritance
- add type hints (from __future__ import annotations) across models/client
Job-level continue-on-error still surfaces the job as a failed check on the
PR; moving it to the mypy step lets the job pass green while mypy stays
advisory.
- get_resourcepolicy: annotate action as Optional[str]. None is a tested,
  supported value that omits the action filter, but the str annotation
  rejected it for typed consumers now that the package ships py.typed.
- Copy the class-level PROXY_DICT default per instance so mutating one
  client's .proxies cannot leak into other default-constructed clients.
- README: drop the deleted requirements.txt install step and the stale
  Python 3.8 requirement (now `pip install .` / Python 3.10+).
The library is consumed as a vendored submodule on sys.path, so the type
hints in the source are read directly and the PEP 561 marker adds nothing;
MANIFEST.in only affects an sdist that is never built here. Remove both and
the now-dangling package-data entry.
publish.sh still ran 'python setup.py bdist_wheel' and uploaded a hardcoded
0.1.10 wheel, and MAINTAINING.md told maintainers to bump setup.py. Build the
sdist and wheel with 'python -m build', check them with twine and upload
dist/*, so the script needs no per-release edit; point the release notes at
project.version in pyproject.toml.
dtq supports 3.8 (setup.py python_requires>=3.8, a 3.8-compatible test helper),
and nothing in this branch needs 3.10 at runtime: the PEP 604 hints stay lazy
under 'from __future__ import annotations'. Restore requires-python >=3.8 and
the classifiers, and lint for 3.8 (ruff target-version, pylint py-version).
…es=None

- dspace_rest_client/__init__.py imported .client eagerly, so importing any
  submodule (eg. dspace_rest_client.models) froze DSpaceClient's env-based
  class defaults (DSPACE_API_ENDPOINT, PROXY_URL, ...) before the application
  had set them. DSpaceClient is now resolved on first attribute access.
- Replacing 'from .models import *' dropped HALResource and ExternalDataObject
  from dspace_rest_client.client; they are imported and re-exported again.
- An explicit proxies=None was turned into PROXY_DICT. A private sentinel is
  the default now, so only an omitted argument means PROXY_DICT (still copied
  per instance) and None is passed to requests as before.
Both are typed 'dict | None = None' but tested 'name in api_resource' on it.
Treat None as an empty resource, like Bitstream does.
@vidiecan
vidiecan force-pushed the feat/pyproject-tooling-typing branch from b2af3a6 to 64dbd11 Compare October 7, 2026 07:35
@vidiecan

vidiecan commented Oct 7, 2026

Copy link
Copy Markdown
Author

Rebased onto dtq a62a63e, plus review fixes

Rebase. The branch is now on top of dtq a62a63e (#20, #27, #28, #29). There were conflicts in client.py and models.py. They were resolved like this:

Review fixes. Each one comes with a test.

Finding Change Test
publish.sh still ran the deleted setup.py; MAINTAINING.md said to bump setup.py publish.sh now runs python -m build, then twine check, then uploads dist/* (no hard-coded wheel name). MAINTAINING.md points at project.version in pyproject.toml built locally: sdist and wheel pass twine check
requires-python = ">=3.10" conflicted with dtq's 3.8 support Back to >=3.8 with the 3.8/3.9 classifiers restored. Ruff target-version and pylint py-version are set to 3.8, and the README says 3.8+. Nothing needs 3.10 at runtime because PEP 604 hints stay lazy under from __future__ import annotations full suite passes on Python 3.8.20 and 3.12; vermin -t=3.8- reports min 3.8
__init__ eagerly imported .client, which froze the env-based DSpaceClient defaults on the first import of any submodule __init__ no longer imports .client. DSpaceClient is resolved on first access through a module __getattr__, and __all__ and the model re-exports are unchanged tests/test_packaging.py: importing dspace_rest_client.models does not load client; an env var set after that import reaches DSpaceClient.API_ENDPOINT; dspace_rest_client.DSpaceClient still resolves
Removing from .models import * dropped HALResource and ExternalDataObject from dspace_rest_client.client Both are imported again, and every formerly star-imported model is listed in client.__all__ test_models_previously_star_imported_into_client_stay_importable
An explicit proxies=None became PROXY_DICT The default is now a private sentinel. Only an omitted argument means PROXY_DICT (still copied per instance), and None is passed through as before test_proxies_default_to_proxy_dict, test_explicit_proxies_none_is_kept
Group/User were typed dict | None = None but ran 'name' in api_resource with no guard api_resource = api_resource or {}, the same as Bitstream test_constructible_without_api_resource

Checks: pytest 94 passed on 3.8 and 3.12. Ruff 0.12.7 is clean and pylint 3.3.7 scores 10.00/10. mypy is non-blocking: 36 findings, down from 54 at the rebased head before these fixes.

Not done here: mypy still targets python_version = "3.10" (it is non-blocking, and recent mypy no longer targets 3.8). CI still tests only 3.10 and 3.12, because ubuntu-latest has no 3.8 runner. 3.8 was verified locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants