diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 629319a..6b9e06e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,32 +1,62 @@
---
name: Bug report
-about: Create a report to help us improve
-title: ''
+about: Create a bug report to help us improve.
+title: "bug: "
labels: unconfirmed
assignees: ''
---
-**Describe the bug**
-A clear and concise description of what the bug is.
+### Description of the bug
+
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Run command '...'
-3. Scroll down to '...'
-4. See error
+### To Reproduce
+
-**System (please complete the following information):**
-- `Auto-Denoise` version: [e.g. 0.2.1]
-- Python version: [e.g. 3.10]
-- OS: [Windows/Linux]
+```
+WRITE MRE / INSTRUCTIONS HERE
+```
-**Additional context**
-Add any other context about the problem here.
+### Full traceback
+
+
+Full traceback
+
+```python
+PASTE TRACEBACK HERE
+```
+
+
+
+### Expected behavior
+
+
+### Environment information
+
+
+```bash
+autoden --debug-info # | xclip -selection clipboard
+```
+
+PASTE OUTPUT HERE
+
+### Additional context
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3cde240
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+- name: I have a question / I need help
+ url: https://github.com/CEA-MetroCarac/auto-denoise/discussions/new?category=q-a
+ about: Ask and answer questions in the Discussions tab.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 4fe86d5..a57a119 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,20 +1,20 @@
---
name: Feature request
-about: Suggest an idea for this project
-title: ''
+about: Suggest an idea for this project.
+title: "feature: "
labels: feature
assignees: ''
---
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+### Is your feature request related to a problem? Please describe.
+
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
+### Describe the solution you'd like
+
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+### Describe alternatives you've considered
+
-**Additional context**
-Add any other context or screenshots about the feature request here.
+### Additional context
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f77703c..a1f5bc8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: ci
+name: Python package
on:
push:
@@ -16,14 +16,7 @@ env:
LANG: en_US.utf-8
LC_ALL: en_US.utf-8
PYTHONIOENCODING: UTF-8
-
- # To fix an error when running Poetry on Windows
- # (https://github.com/python-poetry/poetry/issues/2629),
- # we set Poetry's cache directory to .poetry_cache in the current directory.
- # It makes it easier to later remove the virtualenv when it's broken.
- # Absolute path is necessary to avoid this issue:
- # https://github.com/python-poetry/poetry/issues/3049
- POETRY_CACHE_DIR: ${{ github.workspace }}/.poetry_cache
+ PYTHON_VERSIONS: ""
jobs:
@@ -35,42 +28,41 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Fetch all tags
+ run: git fetch --depth=1 --tags
+
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.10"
- - name: Set up Poetry
- run: pip install poetry
-
- - name: Set up the cache
- uses: actions/cache@v3
- with:
- path: .poetry_cache
- key: quality-poetry-cache
-
- - name: Set up the project
- run: poetry install -vv
-
- - name: Check if the documentation builds correctly
- run: poetry run duty check-docs
-
- - name: Check the code quality
- run: poetry run duty check-code-quality
+ - name: Python info
+ shell: bash -e {0}
+ run: |
+ which python
+ python --version
- - name: Check if the code is correctly typed
- run: poetry run duty check-types
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools build
+ python -m pip install flake8 pytest
+ python -m pip install .
- - name: Check for vulnerabilities in dependencies
+ - name: Lint with flake8
run: |
- pip install safety
- poetry run duty check-dependencies
+ # stop the build if there are Python syntax errors or undefined names
+ flake8 src/autoden tests examples --count --select=E9,F63,F7,F82 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 src/autoden tests examples --count --exit-zero --max-complexity=15 --max-line-length=127 --statistics
tests:
strategy:
matrix:
- os: [ ubuntu-latest, macos-latest, windows-latest ]
+ os:
+ - ubuntu-latest
+ # - windows-latest
+ # - macos-latest
python-version: [ "3.10", "3.11", "3.12" ]
runs-on: ${{ matrix.os }}
@@ -80,21 +72,21 @@ jobs:
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
- - name: Set up Poetry
- run: pip install poetry
-
- - name: Set up the cache
- uses: actions/cache@v3
- with:
- path: .poetry_cache
- key: tests-poetry-cache-${{ matrix.os }}-py${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools build
+ python -m pip install flake8 pytest
+ python -m pip install .
- - name: Set up the project
- run: poetry install -vv || { rm -rf .poetry_cache/virtualenvs/*; poetry install -vv; }
+ - name: Test with pytest
+ run: |
+ pytest tests/
- - name: Run the test suite
- run: poetry run duty test
+ - name: Verify that we can build the package
+ run: |
+ python -m build
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 68bc17f..ac9d074 100644
--- a/.gitignore
+++ b/.gitignore
@@ -140,6 +140,7 @@ venv.bak/
# mypy
.mypy_cache/
+.ruff_cache/
.dmypy.json
dmypy.json
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 712c5cb..898f5f7 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -2,73 +2,132 @@
## Our Pledge
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of experience,
-nationality, personal appearance, race, religion, or sexual identity and
-orientation.
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
## Our Standards
-Examples of behavior that contributes to creating a positive environment
-include:
+Examples of behavior that contributes to a positive environment for our
+community include:
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
-Examples of unacceptable behavior by participants include:
+Examples of unacceptable behavior include:
-* The use of sexualized language or imagery and unwelcome sexual attention or
-advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
-## Our Responsibilities
+## Enforcement Responsibilities
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
## Scope
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at nicola.vigano@cea.fr. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
+reported to the community leaders responsible for enforcement at
+nicola.vigano@cea.fr.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at [http://contributor-covenant.org/version/1/4][version]
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d7093e9..9674ee0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,24 +14,26 @@ cd auto-denoise
make setup
```
-!!! note
- If it fails for some reason,
- you'll need to install
- [Poetry](https://github.com/python-poetry/poetry)
- manually.
-
- You can install it with:
-
- ```bash
- python3 -m pip install --user pipx
- pipx install poetry
- ```
-
- Now you can try running `make setup` again,
- or simply `poetry install`.
+> NOTE:
+> If it fails for some reason,
+> you'll need to install
+> [uv](https://github.com/astral-sh/uv)
+> manually.
+>
+> You can install it with:
+>
+> ```bash
+> python3 -m pip install --user pipx
+> pipx install uv
+> ```
+>
+> Now you can try running `make setup` again,
+> or simply `uv install`.
You now have the dependencies installed.
+You can run the application with `make run autoden [ARGS...]`.
+
Run `make help` to see all the available actions!
## Tasks
@@ -39,35 +41,30 @@ Run `make help` to see all the available actions!
This project uses [duty](https://github.com/pawamoy/duty) to run tasks.
A Makefile is also provided. The Makefile will try to run certain tasks
on multiple Python versions. If for some reason you don't want to run the task
-on multiple Python versions, you can do one of the following:
-
-1. `export PYTHON_VERSIONS= `: this will run the task
- with only the current Python version
-2. run the task directly with `poetry run duty TASK`,
- or `duty TASK` if the environment was already activated
- through `poetry shell`
+on multiple Python versions, you run the task directly with `make run duty TASK`.
-The Makefile detects if the Poetry environment is activated,
+The Makefile detects if a virtual environment is activated,
so `make` will work the same with the virtualenv activated or not.
+If you work in VSCode, we provide
+[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup)
+for the project.
+
## Development
As usual:
-1. create a new branch: `git checkout -b feature-or-bugfix-name`
+1. create a new branch: `git switch -c feature-or-bugfix-name`
1. edit the code and/or the documentation
-If you updated the documentation or the project dependencies:
-
-1. run `make docs-regen`
-1. run `make docs-serve`,
- go to http://localhost:8000 and check that everything looks good
-
**Before committing:**
1. run `make format` to auto-format the code
1. run `make check` to check everything (fix any warning)
1. run `make test` to run the tests (fix any issue)
+1. if you updated the documentation or the project dependencies:
+ 1. run `make docs`
+ 1. go to http://localhost:8000 and check that everything looks good
1. follow our [commit message convention](#commit-message-convention)
If you are unsure about how to fix or ignore a warning,
@@ -78,8 +75,9 @@ Don't bother updating the changelog, we will take care of this.
## Commit message convention
-Commits messages must follow the
-[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message):
+Commit messages must follow our convention based on the
+[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message)
+or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html):
```
[(scope)]: Subject
@@ -87,34 +85,52 @@ Commits messages must follow the
[Body]
```
+**Subject and body must be valid Markdown.**
+Subject must have proper casing (uppercase for first letter
+if it makes sense), but no dot at the end, and no punctuation
+in general.
+
Scope and body are optional. Type can be:
- `build`: About packaging, building wheels, etc.
- `chore`: About packaging or repo/files management.
- `ci`: About Continuous Integration.
+- `deps`: Dependencies update.
- `docs`: About documentation.
- `feat`: New feature.
- `fix`: Bug fix.
- `perf`: About performance.
-- `refactor`: Changes which are not features nor bug fixes.
+- `refactor`: Changes that are not features or bug fixes.
- `style`: A change in code style/format.
- `tests`: About tests.
-**Subject (and body) must be valid Markdown.**
-If you write a body, please add issues references at the end:
+If you write a body, please add trailers at the end
+(for example issues and PR references, or co-authors),
+without relying on GitHub's flavored Markdown:
```
Body.
-References: #10, #11.
-Fixes #15.
+Issue #10: https://github.com/namespace/project/issues/10
+Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15
```
+These "trailers" must appear at the end of the body,
+without any blank lines between them. The trailer title
+can contain any character except colons `:`.
+We expect a full URI for each trailer, not just GitHub autolinks
+(for example, full GitHub URLs for commits and issues,
+not the hash or the #issue-number).
+
+We do not enforce a line length on commit messages summary and body,
+but please avoid very long summaries, and very long lines in the body,
+unless they are part of code blocks that must not be wrapped.
+
## Pull requests guidelines
Link to any related issue in the Pull Request message.
-During review, we recommend using fixups:
+During the review, we recommend using fixups:
```bash
# SHA is the SHA of the commit you want to fix
@@ -124,7 +140,7 @@ git commit --fixup=SHA
Once all the changes are approved, you can squash your commits:
```bash
-git rebase -i --autosquash master
+git rebase -i --autosquash main
```
And force-push:
diff --git a/Makefile b/Makefile
index 56c7b5f..5e88121 100644
--- a/Makefile
+++ b/Makefile
@@ -1,45 +1,28 @@
-.DEFAULT_GOAL := help
-SHELL := bash
+# If you have `direnv` loaded in your shell, and allow it in the repository,
+# the `make` command will point at the `scripts/make` shell script.
+# This Makefile is just here to allow auto-completion in the terminal.
-DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo poetry run) duty
-
-args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)"))
-check_code_quality_args = files
-docs_serve_args = host port
-release_args = version
-test_args = match
-
-BASIC_DUTIES = \
+actions = \
+ allrun \
changelog \
+ check \
+ check-api \
+ check-docs \
+ check-quality \
+ check-types \
clean \
coverage \
docs \
docs-deploy \
- docs-regen \
- docs-serve \
format \
- release
-
-QUALITY_DUTIES = \
- check \
- check-code-quality \
- check-dependencies \
- check-docs \
- check-types \
- test
-
-.PHONY: help
-help:
- @$(DUTY) --list
-
-.PHONY: setup
-setup:
- @bash scripts/setup.sh
-
-.PHONY: $(BASIC_DUTIES)
-$(BASIC_DUTIES):
- @$(DUTY) $@ $(call args,$@)
-
-.PHONY: $(QUALITY_DUTIES)
-$(QUALITY_DUTIES):
- @bash scripts/multirun.sh duty $@ $(call args,$@)
+ help \
+ multirun \
+ release \
+ run \
+ setup \
+ test \
+ vscode
+
+.PHONY: $(actions)
+$(actions):
+ @python scripts/make "$@"
diff --git a/conda/meta.yaml b/conda/meta.yaml
index 48d7d26..b0f5a4d 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -1,10 +1,11 @@
-{% set pyproject = load_file_data('pyproject.toml') %}
-{% set poetry = pyproject.get('tool', {}).get('poetry') %}
+{%- set pyproject = load_file_data('pyproject.toml') %}
+{%- set project = pyproject.get('project', {}) %}
+{%- set env_file = load_file_data('environment.yaml') %}
package:
- name: {{ poetry.get('name') }}
- version: {{ poetry.get('version') }}
+ name: {{ project['name'] }}
+ version: {{ project['version'] }}
source:
path: ../
@@ -22,17 +23,21 @@ requirements:
run:
- python
- # dependencies are defined in pyproject.toml
- {% for dep in poetry.get('dependencies') %}
- - {{ dep.lower() }}
+ # Dependencies as defined in environment.yaml
+ # Some names might be different from pypi, so please curate that list manually
+ {% for dep in env_file['dependencies'] %}
+ - {{ dep }}
{% endfor %}
about:
- home: {{ poetry.get('repository') }}
+ home: {{ project['urls']['Homepage'] }}
license_file: LICENSE
- summary: {{ poetry.get('description') }}
- doc_url: {{ poetry.get('documentation') }}
+ summary: {{ project['description'] }}
+ doc_url: {{ project['urls']['Documentation'] }}
extra:
maintainers:
- - {{ poetry.get('authors') }}
+ {% for author in project['authors'] %}
+ - "{{ author.name }} <{{ author.email }}>"
+ {% endfor %}
+
diff --git a/config/coverage.ini b/config/coverage.ini
index a996004..b56a286 100644
--- a/config/coverage.ini
+++ b/config/coverage.ini
@@ -1,24 +1,25 @@
-[coverage:paths]
-source =
- src/autoden
- */site-packages/autoden
-
[coverage:run]
branch = true
-source =
- src/autoden
- tests
parallel = true
+source =
+ src/
+ tests/
+
+[coverage:paths]
+equivalent =
+ src/
+ .venv/lib/*/site-packages/
+ .venvs/*/lib/*/site-packages/
[coverage:report]
precision = 2
omit =
- src/autoden/__init__.py
- src/autoden/__main__.py
- tests/*
-
-[coverage:html]
-directory = build/coverage
+ src/*/__init__.py
+ src/*/__main__.py
+ tests/__init__.py
+exclude_lines =
+ pragma: no cover
+ if TYPE_CHECKING
[coverage:json]
-output = build/coverage.json
+output = htmlcov/coverage.json
diff --git a/config/git-changelog.toml b/config/git-changelog.toml
new file mode 100644
index 0000000..57114e0
--- /dev/null
+++ b/config/git-changelog.toml
@@ -0,0 +1,9 @@
+bump = "auto"
+convention = "angular"
+in-place = true
+output = "CHANGELOG.md"
+parse-refs = false
+parse-trailers = true
+sections = ["build", "deps", "feat", "fix", "refactor"]
+template = "keepachangelog"
+versioning = "pep440"
diff --git a/config/mypy.ini b/config/mypy.ini
index 98efbae..814e2ac 100644
--- a/config/mypy.ini
+++ b/config/mypy.ini
@@ -1,3 +1,5 @@
[mypy]
ignore_missing_imports = true
exclude = tests/fixtures/
+warn_unused_ignores = true
+show_error_codes = true
diff --git a/config/pytest.ini b/config/pytest.ini
index ad72bbe..052a2f1 100644
--- a/config/pytest.ini
+++ b/config/pytest.ini
@@ -1,16 +1,14 @@
[pytest]
-norecursedirs =
- .git
- .tox
- .env
- dist
- build
python_files =
test_*.py
- *_test.py
- tests.py
addopts =
--cov
--cov-config config/coverage.ini
testpaths =
tests
+
+# action:message_regex:warning_class:module_regex:line
+filterwarnings =
+ error
+ # TODO: remove once pytest-xdist 4 is released
+ ignore:.*rsyncdir:DeprecationWarning:xdist
diff --git a/config/ruff.toml b/config/ruff.toml
new file mode 100644
index 0000000..5c902d3
--- /dev/null
+++ b/config/ruff.toml
@@ -0,0 +1,84 @@
+target-version = "py38"
+line-length = 120
+
+[lint]
+exclude = [
+ "tests/fixtures/*.py",
+]
+select = [
+ "A", "ANN", "ARG",
+ "B", "BLE",
+ "C", "C4",
+ "COM",
+ "D", "DTZ",
+ "E", "ERA", "EXE",
+ "F", "FBT",
+ "G",
+ "I", "ICN", "INP", "ISC",
+ "N",
+ "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI",
+ "Q",
+ "RUF", "RSE", "RET",
+ "S", "SIM", "SLF",
+ "T", "T10", "T20", "TCH", "TID", "TRY",
+ "UP",
+ "W",
+ "YTT",
+]
+ignore = [
+ "A001", # Variable is shadowing a Python builtin
+ "ANN101", # Missing type annotation for self
+ "ANN102", # Missing type annotation for cls
+ "ANN204", # Missing return type annotation for special method __str__
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
+ "ARG005", # Unused lambda argument
+ "C901", # Too complex
+ "D105", # Missing docstring in magic method
+ "D417", # Missing argument description in the docstring
+ "E501", # Line too long
+ "ERA001", # Commented out code
+ "G004", # Logging statement uses f-string
+ "PLR0911", # Too many return statements
+ "PLR0912", # Too many branches
+ "PLR0913", # Too many arguments to function call
+ "PLR0915", # Too many statements
+ "SLF001", # Private member accessed
+ "TRY003", # Avoid specifying long messages outside the exception class
+]
+
+[lint.per-file-ignores]
+"src/*/cli.py" = [
+ "T201", # Print statement
+]
+"src/*/debug.py" = [
+ "T201", # Print statement
+]
+"scripts/*.py" = [
+ "INP001", # File is part of an implicit namespace package
+ "T201", # Print statement
+]
+"tests/*.py" = [
+ "ARG005", # Unused lambda argument
+ "FBT001", # Boolean positional arg in function definition
+ "PLR2004", # Magic value used in comparison
+ "S101", # Use of assert detected
+]
+
+[lint.flake8-quotes]
+docstring-quotes = "double"
+
+[lint.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+[lint.isort]
+known-first-party = ["autoden"]
+
+[lint.pydocstyle]
+convention = "google"
+
+[format]
+exclude = [
+ "tests/fixtures/*.py",
+]
+docstring-code-format = true
+docstring-code-line-length = 80
diff --git a/config/vscode/launch.json b/config/vscode/launch.json
new file mode 100644
index 0000000..e328838
--- /dev/null
+++ b/config/vscode/launch.json
@@ -0,0 +1,47 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "python (current file)",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${file}",
+ "console": "integratedTerminal",
+ "justMyCode": false
+ },
+ {
+ "name": "docs",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "mkdocs",
+ "justMyCode": false,
+ "args": [
+ "serve",
+ "-v"
+ ]
+ },
+ {
+ "name": "test",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "pytest",
+ "justMyCode": false,
+ "args": [
+ "-c=config/pytest.ini",
+ "-vvv",
+ "--no-cov",
+ "--dist=no",
+ "tests",
+ "-k=${input:tests_selection}"
+ ]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "tests_selection",
+ "type": "promptString",
+ "description": "Tests selection",
+ "default": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/config/vscode/settings.json b/config/vscode/settings.json
new file mode 100644
index 0000000..949856d
--- /dev/null
+++ b/config/vscode/settings.json
@@ -0,0 +1,33 @@
+{
+ "files.watcherExclude": {
+ "**/.venv*/**": true,
+ "**/.venvs*/**": true,
+ "**/venv*/**": true
+ },
+ "mypy-type-checker.args": [
+ "--config-file=config/mypy.ini"
+ ],
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "python.testing.pytestArgs": [
+ "--config-file=config/pytest.ini"
+ ],
+ "ruff.enable": true,
+ "ruff.format.args": [
+ "--config=config/ruff.toml"
+ ],
+ "ruff.lint.args": [
+ "--config=config/ruff.toml"
+ ],
+ "yaml.schemas": {
+ "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml"
+ },
+ "yaml.customTags": [
+ "!ENV scalar",
+ "!ENV sequence",
+ "!relative scalar",
+ "tag:yaml.org,2002:python/name:materialx.emoji.to_svg",
+ "tag:yaml.org,2002:python/name:materialx.emoji.twemoji",
+ "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format"
+ ]
+}
\ No newline at end of file
diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json
new file mode 100644
index 0000000..73145ee
--- /dev/null
+++ b/config/vscode/tasks.json
@@ -0,0 +1,97 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "changelog",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["changelog"]
+ },
+ {
+ "label": "check",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check"]
+ },
+ {
+ "label": "check-quality",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-quality"]
+ },
+ {
+ "label": "check-types",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-types"]
+ },
+ {
+ "label": "check-docs",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-docs"]
+ },
+ {
+ "label": "check-api",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-api"]
+ },
+ {
+ "label": "clean",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["clean"]
+ },
+ {
+ "label": "docs",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["docs"]
+ },
+ {
+ "label": "docs-deploy",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["docs-deploy"]
+ },
+ {
+ "label": "format",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["format"]
+ },
+ {
+ "label": "release",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["release", "${input:version}"]
+ },
+ {
+ "label": "setup",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["setup"]
+ },
+ {
+ "label": "test",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["test", "coverage"],
+ "group": "test"
+ },
+ {
+ "label": "vscode",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["vscode"]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "version",
+ "type": "promptString",
+ "description": "Version"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
index 90cb31c..786b75d 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1 +1 @@
---8<-- "CHANGELOG.md"
\ No newline at end of file
+--8<-- "CHANGELOG.md"
diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md
index c2cf022..01f2ea2 100644
--- a/docs/code_of_conduct.md
+++ b/docs/code_of_conduct.md
@@ -1 +1 @@
---8<-- "CODE_OF_CONDUCT.md"
\ No newline at end of file
+--8<-- "CODE_OF_CONDUCT.md"
diff --git a/docs/contributing.md b/docs/contributing.md
index e079654..ea38c9b 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -1 +1 @@
---8<-- "CONTRIBUTING.md"
\ No newline at end of file
+--8<-- "CONTRIBUTING.md"
diff --git a/docs/credits.md b/docs/credits.md
new file mode 100644
index 0000000..f758db8
--- /dev/null
+++ b/docs/credits.md
@@ -0,0 +1,10 @@
+---
+hide:
+- toc
+---
+
+
+```python exec="yes"
+--8<-- "scripts/gen_credits.py"
+```
+
diff --git a/docs/css/material.css b/docs/css/material.css
new file mode 100644
index 0000000..9e8c14a
--- /dev/null
+++ b/docs/css/material.css
@@ -0,0 +1,4 @@
+/* More space at the bottom of the page. */
+.md-main__inner {
+ margin-bottom: 1.5rem;
+}
diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css
index 54950fb..88c7357 100644
--- a/docs/css/mkdocstrings.css
+++ b/docs/css/mkdocstrings.css
@@ -1,33 +1,27 @@
/* Indentation. */
div.doc-contents:not(.first) {
padding-left: 25px;
- border-left: 4px solid rgba(230, 230, 230);
- margin-bottom: 80px;
+ border-left: .05rem solid var(--md-typeset-table-color);
}
-/* Don't capitalize names. */
-h5.doc-heading {
- text-transform: none !important;
-}
-
-/* Don't use vertical space on hidden ToC entries. */
-.hidden-toc::before {
- margin-top: 0 !important;
- padding-top: 0 !important;
-}
+/* Mark external links as such. */
+a.external::after,
+a.autorefs-external::after {
+ /* https://primer.style/octicons/arrow-up-right-24 */
+ mask-image: url('data:image/svg+xml,');
+ -webkit-mask-image: url('data:image/svg+xml,');
+ content: ' ';
-/* Don't show permalink of hidden ToC entries. */
-.hidden-toc a.headerlink {
- display: none;
-}
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
-/* Avoid breaking parameters name, etc. in table cells. */
-td code {
- word-break: normal !important;
+ height: 1em;
+ width: 1em;
+ background-color: currentColor;
}
-/* For pieces of Markdown rendered in table cells. */
-td p {
- margin-top: 0 !important;
- margin-bottom: 0 !important;
+a.external:hover::after,
+a.autorefs-external:hover::after {
+ background-color: var(--md-accent-fg-color);
}
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 4ab0748..612c7a5 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1 +1 @@
---8<-- "README.md"
\ No newline at end of file
+--8<-- "README.md"
diff --git a/docs/license.md b/docs/license.md
index e34968b..a873d2b 100644
--- a/docs/license.md
+++ b/docs/license.md
@@ -1,3 +1,5 @@
+# License
+
```
--8<-- "LICENSE"
-```
\ No newline at end of file
+```
diff --git a/docs/macros.py b/docs/macros.py
deleted file mode 100644
index e279cdb..0000000
--- a/docs/macros.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Macros and filters made available in Markdown pages."""
-
-from itertools import chain
-from pathlib import Path
-
-import toml
-from pip._internal.commands.show import search_packages_info # noqa: WPS436 (no other way?)
-
-
-def get_credits_data() -> dict:
- """
- Return data used to generate the credits file.
-
- Returns:
- Data required to render the credits template.
- """
- project_dir = Path(__file__).parent.parent
- metadata = toml.load(project_dir / "pyproject.toml")["tool"]["poetry"]
- lock_data = toml.load(project_dir / "poetry.lock")
- project_name = metadata["name"]
-
- direct_dependencies = set(metadata["dependencies"].keys())
- direct_dependencies.remove("python")
- dev_dependencies = set(metadata["dev-dependencies"].keys())
- poetry_dependencies = set(chain(direct_dependencies, dev_dependencies))
- indirect_dependencies = {
- pkg["name"].lower() for pkg in lock_data["package"] if pkg["name"].lower() not in poetry_dependencies
- }
-
- packages = {}
- all_pkgs = poetry_dependencies.copy()
- all_pkgs.update(indirect_dependencies)
- for pkg in search_packages_info(list(all_pkgs)):
- # NOTE walrus can be used
- name = pkg.name
- if name:
- packages[name.lower()] = {key: getattr(pkg, key) for key in dir(pkg) if not key.startswith("_")}
-
- # all packages might not be credited,
- # like the ones that are now part of the standard library
- # or the ones that are only used on other operating systems,
- # and therefore are not installed,
- # but it's not that important
-
- return {
- "project_name": project_name,
- "direct_dependencies": sorted(direct_dependencies),
- "dev_dependencies": sorted(dev_dependencies),
- "indirect_dependencies": sorted(indirect_dependencies),
- "package_info": packages,
- }
-
-
-def define_env(env):
- """
- Add macros and filters into the Jinja2 environment.
-
- This hook is called by `mkdocs-macros-plugin`
- when building the documentation.
-
- Arguments:
- env: An object used to add macros and filters to the environment.
- """
- env.macro(get_credits_data, "get_credits_data")
diff --git a/duties.py b/duties.py
deleted file mode 100644
index 0f21f31..0000000
--- a/duties.py
+++ /dev/null
@@ -1,357 +0,0 @@
-"""Development tasks."""
-
-import inspect
-import os
-import re
-import sys
-from pathlib import Path
-from shutil import which
-from typing import List, Optional, Pattern
-
-import httpx
-from duty import duty
-from git_changelog.build import Changelog, Version
-from jinja2.sandbox import SandboxedEnvironment
-
-PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "docs/macros.py"))
-PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS)
-PY_SRC = " ".join(PY_SRC_LIST)
-TESTING = os.environ.get("TESTING", "0") in {"1", "true"}
-CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""}
-WINDOWS = os.name == "nt"
-PTY = not WINDOWS and not CI
-
-
-def latest(lines: List[str], regex: Pattern) -> Optional[str]:
- """
- Return the last released version.
-
- Arguments:
- lines: Lines of the changelog file.
- regex: A compiled regex to find version numbers.
-
- Returns:
- The last version.
- """
- for line in lines:
- match = regex.search(line)
- if match:
- return match.groupdict()["version"]
- return None
-
-
-def unreleased(versions: List[Version], last_release: str) -> List[Version]:
- """
- Return the most recent versions down to latest release.
-
- Arguments:
- versions: All the versions (released and unreleased).
- last_release: The latest release.
-
- Returns:
- A list of versions.
- """
- for index, version in enumerate(versions):
- if version.tag == last_release:
- return versions[:index]
- return versions
-
-
-def read_changelog(filepath: str) -> List[str]:
- """
- Read the changelog file.
-
- Arguments:
- filepath: The path to the changelog file.
-
- Returns:
- The changelog lines.
- """
- with open(filepath, "r") as changelog_file:
- return changelog_file.read().splitlines()
-
-
-def write_changelog(filepath: str, lines: List[str]) -> None:
- """
- Write the changelog file.
-
- Arguments:
- filepath: The path to the changelog file.
- lines: The lines to write to the file.
- """
- with open(filepath, "w") as changelog_file:
- changelog_file.write("\n".join(lines).rstrip("\n") + "\n")
-
-
-def update_changelog(
- inplace_file: str,
- marker: str,
- version_regex: str,
- template_url: str,
- commit_style: str,
-) -> None:
- """
- Update the given changelog file in place.
-
- Arguments:
- inplace_file: The file to update in-place.
- marker: The line after which to insert new contents.
- version_regex: A regular expression to find currently documented versions in the file.
- template_url: The URL to the Jinja template used to render contents.
- commit_style: The style of commit messages to parse.
- """
- env = SandboxedEnvironment(autoescape=False)
- template = env.from_string(httpx.get(template_url).text)
- changelog = Changelog(".", style=commit_style)
-
- if len(changelog.versions_list) == 1:
- last_version = changelog.versions_list[0]
- if last_version.planned_tag is None:
- planned_tag = "0.1.0"
- last_version.tag = planned_tag
- last_version.url += planned_tag
- last_version.compare_url = last_version.compare_url.replace("HEAD", planned_tag)
-
- lines = read_changelog(inplace_file)
- last_released = latest(lines, re.compile(version_regex))
- if last_released:
- changelog.versions_list = unreleased(changelog.versions_list, last_released)
- rendered = template.render(changelog=changelog, inplace=True)
- lines[lines.index(marker)] = rendered
- write_changelog(inplace_file, lines)
-
-
-@duty
-def changelog(ctx):
- """
- Update the changelog in-place with latest commits.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run(
- update_changelog,
- kwargs={
- "inplace_file": "CHANGELOG.md",
- "marker": "",
- "version_regex": r"^## \[v?(?P[^\}}+)",
- "template_url": "https://raw.githubusercontent.com/pawamoy/jinja-templates/master/keepachangelog.md",
- "commit_style": "angular",
- },
- title="Updating changelog",
- pty=PTY,
- )
-
-
-@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"])
-def check(ctx):
- """
- Check it all!
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
-
-
-@duty
-def check_code_quality(ctx, files=PY_SRC):
- """
- Check the code quality.
-
- Arguments:
- ctx: The context instance (passed automatically).
- files: The files to check.
- """
- ctx.run(f"flake8 --config=config/flake8.ini {files}", title="Checking code quality", pty=PTY)
-
-
-@duty
-def check_dependencies(ctx):
- """
- Check for vulnerabilities in dependencies.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- nofail = False
- safety = which("safety")
- if not safety:
- pipx = which("pipx")
- if pipx:
- safety = f"{pipx} run safety"
- else:
-
- def safety_not_available(): # noqa: WPS430
- print(
- inspect.cleandoc( # noqa: WPS462
- """
- Please install safety or pipx to run this task:
-
- pip install --user safety
- # or
- pip install --user pipx
- # and potentially
- pipx install safety
-
- See https://github.com/advisories/GHSA-7q25-qrjw-6fg2
- """
- ) # noqa: WPS355
- )
- return 1
-
- ctx.run(safety_not_available, title="Checking dependencies", nofail=True)
- return
- ctx.run(
- f"poetry export -f requirements.txt --without-hashes | {safety} check --stdin --full-report",
- title="Checking dependencies",
- pty=PTY,
- nofail=nofail,
- )
-
-
-@duty
-def check_docs(ctx):
- """
- Check if the documentation builds correctly.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- Path("build/coverage").mkdir(parents=True, exist_ok=True)
- Path("build/coverage/index.html").touch(exist_ok=True)
- ctx.run("mkdocs build -s", title="Building documentation", pty=PTY)
-
-
-@duty
-def check_types(ctx):
- """
- Check that the code is correctly typed.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run(f"mypy --config-file config/mypy.ini {PY_SRC}", title="Type-checking", pty=PTY)
-
-
-@duty(silent=True)
-def clean(ctx):
- """
- Delete temporary files.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run("rm -rf .coverage*")
- ctx.run("rm -rf .mypy_cache")
- ctx.run("rm -rf .pytest_cache")
- ctx.run("rm -rf tests/.pytest_cache")
- ctx.run("rm -rf build")
- ctx.run("rm -rf dist")
- ctx.run("rm -rf pip-wheel-metadata")
- ctx.run("rm -rf site")
- ctx.run("find . -type d -name __pycache__ | xargs rm -rf")
- ctx.run("find . -name '*.rej' -delete")
-
-
-@duty
-def docs(ctx):
- """
- Build the documentation locally.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run("mkdocs build", title="Building documentation")
-
-
-@duty
-def docs_serve(ctx, host="127.0.0.1", port=8000):
- """
- Serve the documentation (localhost:8000).
-
- Arguments:
- ctx: The context instance (passed automatically).
- host: The host to serve the docs from.
- port: The port to serve the docs on.
- """
- ctx.run(f"mkdocs serve -a {host}:{port}", title="Serving documentation", capture=False)
-
-
-@duty
-def docs_deploy(ctx):
- """
- Deploy the documentation on GitHub pages.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run("mkdocs gh-deploy", title="Deploying documentation")
-
-
-@duty
-def format(ctx):
- """
- Run formatting tools on the code.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run(
- f"autoflake -ir --exclude tests/fixtures --remove-all-unused-imports {PY_SRC}",
- title="Removing unused imports",
- pty=PTY,
- )
- ctx.run(f"isort {PY_SRC}", title="Ordering imports", pty=PTY)
- ctx.run(f"black {PY_SRC}", title="Formatting code", pty=PTY)
-
-
-@duty
-def release(ctx, version):
- """
- Release a new Python package.
-
- Arguments:
- ctx: The context instance (passed automatically).
- version: The new version number to use.
- """
- ctx.run(f"poetry version {version}", title=f"Bumping version in pyproject.toml to {version}", pty=PTY)
- ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY)
- ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY)
- ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY)
- if not TESTING:
- ctx.run("git push", title="Pushing commits", pty=False)
- ctx.run("git push --tags", title="Pushing tags", pty=False)
- ctx.run("poetry build", title="Building dist/wheel", pty=PTY)
- ctx.run("poetry publish", title="Publishing version", pty=PTY)
- docs_deploy.run() # type: ignore
-
-
-@duty(silent=True)
-def coverage(ctx):
- """
- Report coverage as text and HTML.
-
- Arguments:
- ctx: The context instance (passed automatically).
- """
- ctx.run("coverage combine .coverage-*", nofail=True)
- ctx.run("coverage report --rcfile=config/coverage.ini", capture=False)
- ctx.run("coverage html --rcfile=config/coverage.ini")
-
-
-@duty
-def test(ctx, match: str = ""):
- """
- Run the test suite.
-
- Arguments:
- ctx: The context instance (passed automatically).
- match: A pytest expression to filter selected tests.
- """
- py_version = f"{sys.version_info.major}{sys.version_info.minor}"
- os.environ["COVERAGE_FILE"] = f".coverage-{py_version}"
- ctx.run(
- ["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"],
- title="Running tests",
- pty=PTY,
- )
diff --git a/environment.yaml b/environment.yaml
new file mode 100644
index 0000000..c17970a
--- /dev/null
+++ b/environment.yaml
@@ -0,0 +1,9 @@
+dependencies:
+ - python
+ - numpy>=1.21
+ - scipy
+ - tqdm
+ - matplotlib
+ - imageio
+ - scikit-image
+ - pytorch
\ No newline at end of file
diff --git a/gen_doc_stubs.py b/gen_doc_stubs.py
deleted file mode 100644
index db73a2c..0000000
--- a/gen_doc_stubs.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-Automatic documentation generation.
-"""
-
-from pathlib import Path
-
-import mkdocs_gen_files
-
-src_root = Path("src/autoden")
-for path in src_root.glob("**/*.py"):
- doc_path = Path("reference", path.relative_to(src_root)).with_suffix(".md")
- if path.parts[-1][:2] == "__":
- print(f"Excluding documentation page for: {doc_path}")
- continue
- print(f"Generating documentation page for: {doc_path}")
-
- with mkdocs_gen_files.open(doc_path, "w") as f:
- ident = ".".join(path.with_suffix("").parts[1:])
- print("::: " + ident, file=f)
diff --git a/mkdocs.yml b/mkdocs.yml
index f6fbc61..06384f9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,22 +4,24 @@ site_url: "https://CEA-MetroCarac.github.io/auto-denoise"
repo_url: "https://github.com/CEA-MetroCarac/auto-denoise"
repo_name: "CEA-MetroCarac/auto-denoise"
site_dir: "site"
+watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/autoden]
+copyright: Copyright © 2023 Nicola VIGANO
+edit_uri: edit/main/docs/
+
+validation:
+ omitted_files: warn
+ absolute_links: warn
+ unrecognized_links: warn
nav:
- Home:
- Overview: index.md
- Changelog: changelog.md
+ - Credits: credits.md
- License: license.md
-- Code Reference:
- - algorithms.py: reference/algorithms.md
- - datasets.py: reference/datasets.md
- - losses.py: reference/losses.md
- - models:
- - config.py: reference/models/config.md
- - dncnn.py: reference/models/dncnn.md
- - msd.py: reference/models/msd.md
- - unet.py: reference/models/unet.md
- - cli.py: reference/cli.md
+# defer to gen-files + literate-nav
+- API reference:
+ - Auto-Denoise: reference/
- Development:
- Contributing: contributing.md
- Code of Conduct: code_of_conduct.md
@@ -27,42 +29,109 @@ nav:
theme:
name: material
features:
+ - announce.dismiss
+ - content.action.edit
+ - content.action.view
+ - content.code.annotate
+ - content.code.copy
+ - content.tooltips
+ - navigation.footer
+ - navigation.indexes
+ - navigation.sections
- navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.top
+ - search.highlight
+ - search.suggest
+ - toc.follow
palette:
- - scheme: blue
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: teal
+ accent: purple
toggle:
- icon: material/brightness-7
+ icon: material/weather-sunny
name: Switch to dark mode
- - scheme: slate
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ accent: lime
toggle:
- icon: material/brightness-4
- name: Switch to light mode
+ icon: material/weather-night
+ name: Switch to system preference
extra_css:
+- css/material.css
- css/mkdocstrings.css
markdown_extensions:
+- attr_list
- admonition
-- pymdownx.emoji
+- footnotes
+- pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.magiclink
- pymdownx.snippets:
+ base_path: [!relative $config_dir]
check_paths: true
- pymdownx.superfences
-- pymdownx.tabbed
-- pymdownx.tasklist
+- pymdownx.tabbed:
+ alternate_style: true
+ slugify: !!python/object/apply:pymdownx.slugs.slugify
+ kwds:
+ case: lower
+- pymdownx.tasklist:
+ custom_checkbox: true
- toc:
permalink: true
plugins:
- search
- autorefs
+- literate-nav:
+ nav_file: SUMMARY.md
- mkdocstrings:
handlers:
python:
+ import:
+ - https://docs.python.org/3/objects.inv
+ paths: [src]
options:
docstring_style: numpy
-- macros:
- module_name: docs/macros
+ docstring_options:
+ ignore_init_summary: true
+ docstring_section_style: list
+ filters: ["!^_"]
+ heading_level: 1
+ inherited_members: true
+ merge_init_into_class: true
+ separate_signature: true
+ show_root_heading: true
+ show_root_full_path: false
+ show_signature_annotations: true
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
+ signature_crossrefs: true
+ summary: true
- gen-files:
scripts:
- - gen_doc_stubs.py
+ - scripts/gen_ref_nav.py
+- git-committers:
+ enabled: !ENV [DEPLOY, false]
+ repository: CEA-MetroCarac/auto-denoise
+- group:
+ enabled: !ENV [MATERIAL_INSIDERS, false]
+ plugins:
+ - typeset
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/Obi-Wan
+ - icon: fontawesome/brands/python
+ link: https://pypi.org/project/auto-denoise/
diff --git a/pyproject.toml b/pyproject.toml
index 05e7890..4426246 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,70 +1,84 @@
[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
-[tool.poetry]
+[project]
name = "auto-denoise"
version = "0.1.0"
description = "Unsupervised and self-supervised CNN denoising methods."
-authors = ["Nicola VIGANO "]
-license = "MIT License"
-readme = "README.md"
-repository = "https://github.com/CEA-MetroCarac/auto-denoise"
-homepage = "https://github.com/CEA-MetroCarac/auto-denoise"
-documentation = "https://CEA-MetroCarac.github.com.io/auto-denoise/"
+authors = [{name = "Nicola VIGANO", email = "nicola.vigano@cea.fr"}]
+license = {text = "MIT"}
+readme = {file = "README.md", content-type = "text/markdown"}
+requires-python = ">=3.10"
keywords = []
-packages = [ { include = "autoden", from = "src" } ]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Documentation",
+ "Topic :: Software Development",
+ "Topic :: Utilities",
+ "Typing :: Typed",
+]
+dependencies = [
+ "python>=3.10",
+ "numpy>=1.21",
+ "scipy",
+ "tqdm",
+ "matplotlib",
+ "imageio",
+ "scikit-image",
+ "torch",
+]
-[tool.poetry.dependencies]
-python = ">=3.10"
-numpy = ">=1.21"
-scipy = "*"
-tqdm = "*"
-matplotlib = "*"
-imageio = "*"
-scikit-image = "*"
+[project.optional-dependencies]
+dev = [
+ # dev
+ "editables>=0.5",
-[tool.poetry.extras]
-scikit-image = ["data"]
+ # maintenance
+ "build>=1.2",
+ "git-changelog>=2.4",
+ "twine>=5.1; python_version < '3.13'",
-[tool.poetry.dev-dependencies]
-# formatting, quality, tests
-autoflake = ">=1.4"
-black = "*"
-isort = "*"
-pytest = "*"
-types-toml = ">=0.10.1"
+ # ci
+ "isort",
+ "pyupgrade",
+ "pytest>=8.2",
+ "pytest-cov>=5.0",
+ "pytest-randomly>=3.15",
+ "pytest-xdist>=3.6",
+ "mypy>=1.10",
+ "types-markdown>=3.6",
+ "types-pyyaml>=6.0",
+ "flake8",
-# tasks
-duty = ">=0.6.0"
-git-changelog = ">=0.5.0"
-httpx = ">=0.16.1"
-jinja2-cli = ">=0.7.0"
-toml = ">=0.10.2"
+ # docs
+ "black>=24.4",
+ "mkdocs>=1.6",
+ "mkdocs-gen-files>=0.4",
+ "mkdocs-git-committers-plugin-2>=2.3",
+ "mkdocs-literate-nav>=0.6",
+ "mkdocs-material>=9.5",
+ "mkdocstrings[python]>=0.25",
+ "tomli>=2.0; python_version < '3.11'",
+]
-# flake8 plugins
-flake8 = ">=3.7.0"
-flake8-bandit = ">=2.1.2"
-flake8-black = ">=0.2.1"
-flake8-bugbear = ">=20.11.1"
-flake8-builtins = ">=1.5.3"
-flake8-comprehensions = ">=3.3.1"
-flake8-docstrings = ">=1.5.0"
-flake8-string-format = ">=0.3.0"
-flake8-tidy-imports = ">=4.2.1"
-flake8-variables-names = ">=0.0.4"
-pep8-naming = ">=0.11.1"
-pydocstyle = ">=6.1.1"
+[project.urls]
+Homepage = "https://CEA-MetroCarac.github.io/auto-denoise"
+Documentation = "https://CEA-MetroCarac.github.io/auto-denoise"
+Changelog = "https://CEA-MetroCarac.github.io/auto-denoise/changelog"
+Repository = "https://github.com/CEA-MetroCarac/auto-denoise"
+Issues = "https://github.com/CEA-MetroCarac/auto-denoise/issues"
+Discussions = "https://github.com/CEA-MetroCarac/auto-denoise/discussions"
-# docs
-mkdocs = ">=1.2.2"
-mkdocs-macros-plugin = ">=0.5.0"
-mkdocs-material = ">=6.2.7"
-mkdocstrings = ">=0.16.2"
-
-[tool.poetry.scripts]
+[project.scripts]
autoden = "autoden.cli:main"
-
[tool.black]
line-length = 127
exclude = "tests/fixtures"
diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py
new file mode 100644
index 0000000..0ec7dd4
--- /dev/null
+++ b/scripts/gen_credits.py
@@ -0,0 +1,181 @@
+"""Script to generate the project's credits."""
+
+from __future__ import annotations
+
+import os
+import sys
+from collections import defaultdict
+from importlib.metadata import distributions
+from itertools import chain
+from pathlib import Path
+from textwrap import dedent
+from typing import Dict, Iterable, Union
+
+from jinja2 import StrictUndefined
+from jinja2.sandbox import SandboxedEnvironment
+from packaging.requirements import Requirement
+
+# TODO: Remove once support for Python 3.10 is dropped.
+if sys.version_info >= (3, 11):
+ import tomllib
+else:
+ import tomli as tomllib
+
+project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", "."))
+with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file:
+ pyproject = tomllib.load(pyproject_file)
+project = pyproject["project"]
+project_name = project["name"]
+devdeps = [line.strip() for line in project["optional-dependencies"]["dev"]]
+devdeps = [line for line in devdeps if line and not line.startswith(("-e", "#")) and line != ""]
+# with project_dir.joinpath("requirements-dev.txt").open() as devdeps_file:
+# devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))]
+
+PackageMetadata = Dict[str, Union[str, Iterable[str]]]
+Metadata = Dict[str, PackageMetadata]
+
+
+def _merge_fields(metadata: dict) -> PackageMetadata:
+ fields = defaultdict(list)
+ for header, value in metadata.items():
+ fields[header.lower()].append(value.strip())
+ return {
+ field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0]
+ for field, value in fields.items()
+ }
+
+
+def _norm_name(name: str) -> str:
+ return name.replace("_", "-").replace(".", "-").lower()
+
+
+def _requirements(deps: list[str]) -> dict[str, Requirement]:
+ return {_norm_name((req := Requirement(dep)).name): req for dep in deps}
+
+
+def _extra_marker(req: Requirement) -> str | None:
+ if not req.marker:
+ return None
+ try:
+ return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra")
+ except StopIteration:
+ return None
+
+
+def _get_metadata() -> Metadata:
+ metadata = {}
+ for pkg in distributions():
+ name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore]
+ metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type]
+ metadata[name]["spec"] = set()
+ metadata[name]["extras"] = set()
+ metadata[name].setdefault("summary", "")
+ _set_license(metadata[name])
+ return metadata
+
+
+def _set_license(metadata: PackageMetadata) -> None:
+ license_field = metadata.get("license-expression", metadata.get("license", ""))
+ license_name = license_field if isinstance(license_field, str) else " + ".join(license_field)
+ check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n")
+ if check_classifiers:
+ license_names = []
+ for classifier in metadata["classifier"]:
+ if classifier.startswith("License ::"):
+ license_names.append(classifier.rsplit("::", 1)[1].strip())
+ license_name = " + ".join(license_names)
+ metadata["license"] = license_name or "?"
+
+
+def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata:
+ deps = {}
+ for dep_name, dep_req in base_deps.items():
+ if dep_name not in metadata or dep_name == "auto-denoise":
+ continue
+ metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator]
+ metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator]
+ deps[dep_name] = metadata[dep_name]
+
+ again = True
+ while again:
+ again = False
+ for pkg_name in metadata:
+ if pkg_name in deps:
+ for pkg_dependency in metadata[pkg_name].get("requires-dist", []):
+ requirement = Requirement(pkg_dependency)
+ dep_name = _norm_name(requirement.name)
+ extra_marker = _extra_marker(requirement)
+ if (
+ dep_name in metadata
+ and dep_name not in deps
+ and dep_name != project["name"]
+ and (not extra_marker or extra_marker in deps[pkg_name]["extras"])
+ ):
+ metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator]
+ deps[dep_name] = metadata[dep_name]
+ again = True
+
+ return deps
+
+
+def _render_credits() -> str:
+ metadata = _get_metadata()
+ dev_dependencies = _get_deps(_requirements(devdeps), metadata)
+ prod_dependencies = _get_deps(
+ _requirements(
+ chain( # type: ignore[arg-type]
+ project.get("dependencies", []),
+ chain(*project.get("optional-dependencies", {}).values()),
+ ),
+ ),
+ metadata,
+ )
+
+ template_data = {
+ "project_name": project_name,
+ "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()),
+ "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()),
+ "more_credits": "",
+ }
+ template_text = dedent(
+ """
+ # Credits
+
+ These projects were used to build *{{ project_name }}*. **Thank you!**
+
+ [Python](https://www.python.org/) |
+ [uv](https://github.com/astral-sh/uv) |
+ [copier-uv](https://github.com/pawamoy/copier-uv)
+
+ {% macro dep_line(dep) -%}
+ [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }}
+ {%- endmacro %}
+
+ {% if prod_dependencies -%}
+ ### Runtime dependencies
+
+ Project | Summary | Version (accepted) | Version (last resolved) | License
+ ------- | ------- | ------------------ | ----------------------- | -------
+ {% for dep in prod_dependencies -%}
+ {{ dep_line(dep) }}
+ {% endfor %}
+
+ {% endif -%}
+ {% if dev_dependencies -%}
+ ### Development dependencies
+
+ Project | Summary | Version (accepted) | Version (last resolved) | License
+ ------- | ------- | ------------------ | ----------------------- | -------
+ {% for dep in dev_dependencies -%}
+ {{ dep_line(dep) }}
+ {% endfor %}
+
+ {% endif -%}
+ {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %}
+ """,
+ )
+ jinja_env = SandboxedEnvironment(undefined=StrictUndefined)
+ return jinja_env.from_string(template_text).render(**template_data)
+
+
+print(_render_credits())
diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py
new file mode 100644
index 0000000..6939e86
--- /dev/null
+++ b/scripts/gen_ref_nav.py
@@ -0,0 +1,37 @@
+"""Generate the code reference pages and navigation."""
+
+from pathlib import Path
+
+import mkdocs_gen_files
+
+nav = mkdocs_gen_files.Nav()
+mod_symbol = '
'
+
+root = Path(__file__).parent.parent
+src = root / "src"
+
+for path in sorted(src.rglob("*.py")):
+ module_path = path.relative_to(src).with_suffix("")
+ doc_path = path.relative_to(src).with_suffix(".md")
+ full_doc_path = Path("reference", doc_path)
+
+ parts = tuple(module_path.parts)
+
+ if parts[-1] == "__init__":
+ parts = parts[:-1]
+ doc_path = doc_path.with_name("index.md")
+ full_doc_path = full_doc_path.with_name("index.md")
+ elif parts[-1].startswith("_"):
+ continue
+
+ nav_parts = [f"{mod_symbol} {part}" for part in parts]
+ nav[tuple(nav_parts)] = doc_path.as_posix()
+
+ with mkdocs_gen_files.open(full_doc_path, "w") as fd:
+ ident = ".".join(parts)
+ fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}")
+
+ mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root))
+
+with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
+ nav_file.writelines(nav.build_literate_nav())
diff --git a/scripts/make b/scripts/make
new file mode 100755
index 0000000..e0fcb80
--- /dev/null
+++ b/scripts/make
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+"""Management commands."""
+
+import re
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+import yaml
+
+try:
+ # TODO: Remove once support for Python 3.10 is dropped.
+ if sys.version_info >= (3, 11):
+ import tomllib
+ else:
+ import tomli as tomllib
+except ImportError:
+ print("Could not import tomli/tomlib, please install the package 'build', and then retry")
+
+
+def shell(cmd: str) -> None:
+ """Run a shell command."""
+ print(f"Calling: `{cmd}`")
+ subprocess.run(cmd, shell=True, check=True) # noqa: S602
+
+
+def is_pkg_installed(pkg: str) -> bool:
+ """Checks whether a conda package is installed.
+
+ Parameters
+ ----------
+ pkg : str
+ Package name
+
+ Returns
+ -------
+ bool
+ Whether it is installed.
+ """
+ print(f" * Checking that {pkg} is present..", flush=False, end="")
+ pkg = pkg.split("==")[0]
+ pkg = pkg.split("<=")[0]
+ pkg = pkg.split(">=")[0]
+ pkg = pkg.split("<")[0]
+ pkg = pkg.split(">")[0]
+ result = subprocess.run(f"conda list --full-name {pkg} -e -c", shell=True, check=True, capture_output=True, text=True)
+ is_present = result.stdout.strip() != ""
+ if is_present:
+ print("\b\b: Found!")
+ else:
+ print("\b\b: Not found!")
+ return is_present
+
+
+def _process_dep(dep: str) -> str:
+ mtch = re.match("(?P\w+)\[(?P