[project]
name = "imagecraft"
description = "Create Ubuntu bootable images."
dynamic = ["version", "readme"]
authors = [{ name = "Canonical Ltd.", email = "snapcraft@lists.snapcraft.io" }]
license = { file = "LICENSE" }
dependencies = [
    "craft-parts~=2.6",
    "craft-cli~=2.15",
    "craft-platforms~=0.4",
    "craft-application~=4.6",
    "craft-providers~=2.0",
    "pydantic~=2.8",
]
classifiers = [
    "Development Status :: 1 - Planning",
    "License :: OSI Approved :: GNU General Public License (GPL)",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.12",
]
requires-python = ">=3.11"

[project.urls]
Homepage = "https://github.com/canonical/imagecraft"
Issues = "https://github.com/canonical/imagecraft/issues"
Source = "https://github.com/canonical/imagecraft.git"

[project.scripts]
imagecraft = "imagecraft.cli:run"

[project.optional-dependencies]
types = [
    "mypy[reports]~=1.15.0",
    "types-Pygments",
    "types-colorama",
    "types-setuptools",
]
apt-noble = [
    # 2.7 for Noble
    "python-apt~=2.7.0;sys_platform=='linux'",
]
apt-oracular = [
    # 2.9 for Oracular+
    "python-apt>=2.9.0;sys_platform=='linux'",
]
apt-plucky = [
    # 2.9 for Oracular+
    "python-apt>=2.9.0;sys_platform=='linux'",
]
docs = [
    "canonical-sphinx[full]~=0.3.0",
    "sphinx-autobuild~=2024.2",
    "sphinx-pydantic==0.1.1",
    "sphinx-toolbox~=3.5",
    "sphinx-lint==1.0.0",
    "sphinxcontrib-details-directive",
    "matplotlib",
]


[[tool.uv.index]]
name = "python-apt-wheels"
url = "https://people.canonical.com/~lengau/python-apt-ubuntu-wheels/"


[tool.uv]
constraint-dependencies = [
    # Basic constraints to allow --resolution=lowest
    "build>=0.7.0",
    "cffi>=1.15",
    "iniconfig>=1.1.0",
    "httplib2>=0.20.0",
    "libnacl>=2.0",
    "lxml>=5.0",
    "markdown>=3.0",
    "markupsafe>=2.0",
    "oauthlib>=3.0.0",
    "protobuf>=5.0",
    "pynacl>=1.5",
    "pyparsing>=3.0.0",
    "pyproject-hooks>=1.0.0",
    "pytz>=2020",
    "pyyaml>=5.0",
    "regex>=2021.11.10",
    "setuptools>=50",
    "sphinx-basic-ng>=1.0.0b1",
    "tornado>=4.0",
    "urllib3>=2.0",
    "webencodings>=0.4.0",
    "wheel>=0.38",
]
dev-dependencies = [
    "build",
    "coverage[toml]~=7.4",
    "pytest~=8.0",
    "pytest-cov~=6.0",
    "pytest-check>=2.4",
    "pytest-mock~=3.12",
    "mypy[reports]~=1.15.0",
    "pyright==1.1.393",
    "types-Pygments",
    "types-colorama",
    "types-setuptools",
]
conflicts = [
    [
        { extra = "apt-noble" },
        { extra = "apt-oracular" },
        { extra = "apt-plucky" },
    ],
]


[build-system]
requires = ["setuptools>=69.0", "setuptools_scm[toml]>=7.1"]
build-backend = "setuptools.build_meta"

[tool.setuptools.dynamic]
readme = { file = "README.rst" }

[tool.setuptools_scm]
write_to = "imagecraft/_version.py"
# the version comes from the latest annotated git tag formatted as 'X.Y.Z'
# version scheme:
#   - X.Y.Z.post<commits since tag>+g<hash>.d<%Y%m%d>
# parts of scheme:
#   - X.Y.Z - most recent git tag
#   - post<commits since tag>+g<hash> - present when current commit is not tagged
#   - .d<%Y%m%d> - present when working dir is dirty
# version scheme when no tags exist:
#   - 0.0.post<total commits>+g<hash>
version_scheme = "post-release"
# deviations from the default 'git describe' command:
# - only match annotated tags
# - only match tags formatted as 'X.Y.Z'
git_describe_command = "git describe --dirty --long --match '[0-9]*.[0-9]*.[0-9]*' --exclude '*[^0-9.]*'"

[tool.setuptools.packages.find]
include = ["*craft*"]
namespaces = false

[tool.black]
target-version = ["py311"]

[tool.codespell]
ignore-words-list = "buildd,crate,keyserver,comandos,ro,dedent,dedented"
skip = ".git,build,.*_cache,__pycache__,*.tar,*.snap,*.png,./node_modules,./docs/_build,.direnv,.venv,venv,.vscode"
quiet-level = 3
check-filenames = true

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 88

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = "tests"
xfail_strict = true
markers = ["slow: slow tests"]

[tool.coverage.run]
branch = true
omit = ["tests/**"]

[tool.coverage.report]
skip_empty = true
exclude_also = ["if (typing\\.)?TYPE_CHECKING:"]

[tool.pyright]
strict = ["imagecraft"]
pythonVersion = "3.12"
pythonPlatform = "Linux"
exclude = [
    "**/.*",
    "**/__pycache__",
    # pyright might not like the annotations generated by setuptools_scm
    "**/_version.py",
    "**/tests/lib/external/**",
]

[tool.mypy]
python_version = "3.12"
exclude = ["build", "results"]
warn_unused_configs = true
warn_redundant_casts = true
strict_equality = true
extra_checks = true
warn_return_any = true
disallow_subclassing_any = true
disallow_untyped_decorators = true
disallow_any_generics = true

[[tool.mypy.overrides]]
module = ["imagecraft.*"]
disallow_untyped_defs = true
no_implicit_optional = true

[[tool.mypy.overrides]]
module = ["tests.*"]
strict = false

[tool.ruff]
line-length = 88
target-version = "py310"
src = ["imagecraft", "tests"]
extend-exclude = ["docs", "tests/lib/external", "imagecraft/_version.py"]

[tool.ruff.format]
docstring-code-format = true
line-ending = "lf"
quote-style = "double"

[tool.ruff.lint]
# Follow ST063 - Maintaining and updating linting specifications for updating these.
# Handy link: https://docs.astral.sh/ruff/rules/
select = [ # Base linting rule selections.
    # See the internal document for discussion:
    # https://docs.google.com/document/d/1i1n8pDmFmWi4wTDpk-JfnWCVUThPJiggyPi2DYwBBu4/edit
    # All sections here are stable in ruff and shouldn't randomly introduce
    # failures with ruff updates.
    "F",     # The rules built into Flake8
    "E",
    "W",     # pycodestyle errors and warnings
    "I",     # isort checking
    "N",     # PEP8 naming
    "D",     # Implement pydocstyle checking as well.
    "UP",    # Pyupgrade - note that some of are excluded below due to Python versions
    "YTT",   # flake8-2020: Misuse of `sys.version` and `sys.version_info`
    "ANN",   # Type annotations.
    "ASYNC", # Catching blocking calls in async functions
    # flake8-bandit: security testing. https://docs.astral.sh/ruff/rules/#flake8-bandit-s
    # https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing
    "S101",
    "S102", # assert or exec
    "S103",
    "S108", # File permissions and tempfiles - use #noqa to silence when appropriate.
    "S104", # Network binds
    "S105",
    "S106",
    "S107", # Hardcoded passwords
    "S110", # try-except-pass (use contextlib.suppress instead)
    "S113", # Requests calls without timeouts
    "S3",   # Serialising, deserialising, hashing, crypto, etc.
    "S5",   # Unsafe cryptography or YAML loading.
    "S602", # Subprocess call with shell=true
    "S701", # jinja2 templates without autoescape
    "BLE",  # Do not catch blind exceptions
    "FBT",  # Disallow boolean positional arguments (make them keyword-only)
    "B0",   # Common mistakes and typos.
    "A",    # Shadowing built-ins.
    "COM",  # Trailing commas
    "C4",   # Encourage comprehensions, which tend to be faster than alternatives.
    "T10",  # Don't call the debugger in production code
    "ISC",  # Implicit string concatenation that can cause subtle issues
    "ICN",  # Only use common conventions for import aliases.
    "INP",  # Implicit namespace packages
    # flake8-pie: miscellaneous linters (enabled individually because they're not really related)
    "PIE790", # Unnecessary pass statement
    "PIE794", # Multiple definitions of class field
    "PIE796", # Duplicate value in an enum (reasonable to noqa for backwards compatibility)
    "PIE804", # Don't use a dict with unnecessary kwargs
    "PIE807", # prefer `list` over `lambda: []`
    "PIE810", # Use a tuple rather than multiple calls. E.g. `mystr.startswith(("Hi", "Hello"))`
    "PYI",    # Linting for type stubs.
    "PT",     # Pytest
    "Q",      # Consistent quotations
    "RSE",    # Errors on pytest raises.
    "RET",    # Simpler logic after return, raise, continue or break
    "SLF",    # Prevent accessing private class members.
    "SIM",    # Code simplification
    "TID",    # Tidy imports
    # The team have chosen to only use type-checking blocks when necessary to prevent circular imports.
    # As such, the only enabled type-checking checks are those that warn of an import that needs to be
    # removed from a type-checking block.
    "TC004",  # Remove imports from type-checking guard blocks if used at runtime
    "TC005",  # Delete empty type-checking blocks
    "ARG",    # Unused arguments
    "PTH",    # Migrate to pathlib
    "FIX",    # All TODOs, FIXMEs, etc. should be turned into issues instead.
    "ERA",    # Don't check in commented out code
    "PGH",    # Pygrep hooks
    "PL",     # Pylint
    "TRY",    # Cleaner try/except,
    "FLY",    # Detect things that would be better as f-strings.
    "PERF",   # Catch things that can slow down the application like unnecessary casts to list.
    "RUF001",
    "RUF002",
    "RUF003", # Ambiguous unicode characters
    "RUF005", # Encourages unpacking rather than concatenation
    "RUF008", # Do not use mutable default values for dataclass attributes
    "B035",   # Don't use static keys in dict comprehensions.
    "RUF013", # Prohibit implicit Optionals (PEP 484)
    "RUF100", # #noqa directive that doesn't flag anything
    "RUF200", # If ruff fails to parse pyproject.toml...
]
ignore = [
    "ANN10", # Type annotations for `self` and `cls`
    #"E203",  # Whitespace before ":"  -- Commented because ruff doesn't currently check E203
    "E501",   # Line too long (reason: black will automatically fix this for us)
    "D105",   # Missing docstring in magic method (reason: magic methods already have definitions)
    "D107",   # Missing docstring in __init__ (reason: documented in class docstring)
    "D203",   # 1 blank line required before class docstring (reason: pep257 default)
    "D213",   # Multi-line docstring summary should start at the second line (reason: pep257 default)
    "D215",   # Section underline is over-indented (reason: pep257 default)
    "A003",   # Class attribute shadowing built-in (reason: Class attributes don't often get bare references)
    "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements
    # (reason: this creates long lines that get wrapped and reduces readability)

    # Ignored due to conflicts with ruff's formatter:
    # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
    "COM812", # Missing trailing comma - mostly the same, but marginal differences.
    "ISC001", # Single-line implicit string concatenation.

    # Ignored due to common usage in current code
    "TRY003", # Avoid specifying long messages outside the exception class
]

[tool.ruff.lint.flake8-annotations]
allow-star-arg-any = true

[tool.ruff.lint.pydocstyle]
ignore-decorators = [ # Functions with these decorators don't have to have docstrings.
    "typing.overload", # Default configuration
    # The next four are all variations on override, so child classes don't have to repeat parent classes' docstrings.
    "overrides.override",
    "overrides.overrides",
    "typing.override",
    "typing_extensions.override",
]

[tool.ruff.lint.pylint]
max-args = 8

[tool.ruff.lint.pep8-naming]
# Allow Pydantic's `@validator` decorator to trigger class method treatment.
classmethod-decorators = ["pydantic.validator", "pydantic.root_validator"]

[tool.ruff.lint.per-file-ignores]
"tests/**.py" = [ # Some things we want for the main project are unnecessary in tests.
    "D",       # Ignore docstring rules in tests
    "ANN",     # Ignore type annotations in tests
    "ARG",     # Allow unused arguments in tests (e.g. for fake functions/methods/classes)
    "S101",    # Allow assertions in tests
    "S103",    # Allow `os.chmod` setting a permissive mask `0o555` on file or directory
    "S108",    # Allow Probable insecure usage of temporary file or directory
    "PLR0913", # Allow many arguments for test functions (useful if we need many fixtures)
    "PLR2004", # Allow magic values in tests
    "SLF",     # Allow accessing private members from tests.
]
"__init__.py" = [
    "I001", # isort leaves init files alone by default, this makes ruff ignore them too.
    "F401", # Allows unused imports in __init__ files.
]