Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: merge pydantic 2 support to main #1817

Merged
merged 64 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
22c1a49
build!: Update to pydantic 2
lengau Jul 31, 2024
0fba6af
chore: run bump-pydantic
lengau Jul 31, 2024
9ca1e06
chore: make most model tests pass
lengau Jul 31, 2024
aef7194
fix: fix models unit tests
lengau Jul 31, 2024
05ea547
chore: fix some pydantic warnings
lengau Jul 31, 2024
0a4c156
fix: all unit tests but package service
lengau Jul 31, 2024
a917c95
fix(tests)!: deprecate `build-for: all`
lengau Jul 31, 2024
ed71a5b
replace `dict()` with `model_dump()` in two places
lengau Jul 31, 2024
6eb20f3
chore: remove unused package module
lengau Jul 31, 2024
b9389e8
chore: remove unused providers module
lengau Jul 31, 2024
9da46c3
chore: remove unused store command implementations
lengau Jul 31, 2024
edde43a
chore: remove unused version command
lengau Jul 31, 2024
debf4b2
chore: use upstream `format_pydantic_errors`
lengau Jul 31, 2024
ae66cdf
chore: remove unused deprecations module
lengau Jul 31, 2024
1eacdcd
chore: remove unused env functions
lengau Jul 31, 2024
c148658
chore: remove unused error classe
lengau Jul 31, 2024
7d10f6e
chore: autoformat
lengau Jul 31, 2024
4b8139b
chore: remove unused manifest code
lengau Jul 31, 2024
eb8efea
chore: remove unused create_config_yaml
lengau Jul 31, 2024
309a69d
chore: remove unused create_actions_yaml
lengau Jul 31, 2024
1b41f1c
chore: remove unused create_metadata_yaml function
lengau Jul 31, 2024
99c33ee
chore: remove the unused linters.analyze function
lengau Jul 31, 2024
8b759b1
chore: remove unused config.load function
lengau Jul 31, 2024
b1c33b7
chore: replace bundle_config and config fixtures
lengau Jul 31, 2024
a6f82eb
chore: remove the unused CharmcraftConfig model
lengau Jul 31, 2024
9df8be1
chore: remove unused CharmcraftConfig model
lengau Jul 31, 2024
13560c9
chore: remove unused parse_bundle_metadata_yaml fn
lengau Jul 31, 2024
f130b2f
chore: remove unused file parsers
lengau Jul 31, 2024
d3acb9a
chore: remove unused parse_charm_metadata_yaml
lengau Jul 31, 2024
d760a18
chore: remove unused metafiles module
lengau Jul 31, 2024
f55d1fe
Merge branch 'work/cleanup' into work/1768/pydantic-2
lengau Jul 31, 2024
7ea79c0
fix: make charmplugin requirements validator 'after'
lengau Jul 31, 2024
69ff323
fix: use the correct type for charmhub config
lengau Jul 31, 2024
f00648f
chore: autoformat
lengau Jul 31, 2024
bed3814
build(deps): update doc dependencies for pydantic
lengau Jul 31, 2024
33b9b99
chore: update documentation requriements
lengau Jul 31, 2024
99d6d50
chore: update requirements files
lengau Aug 1, 2024
6038227
feat: update snapcraft.yaml to build with pydantic 2
lengau Aug 2, 2024
b360206
chore: autoformat
lengau Aug 2, 2024
0d3788e
chore: remove unused extension model
lengau Aug 2, 2024
cd21f03
chore: fix mypy issue
lengau Aug 2, 2024
1873ba9
style(type): silence mypy
lengau Aug 2, 2024
db18186
fix(parts): give plugins a default source location
lengau Aug 8, 2024
2a3462f
tests: fix unit tests for craft-application changes
lengau Aug 8, 2024
6b21fe1
fix(project): preprocess the parts in order
lengau Aug 8, 2024
dc19aae
tests(integration): fix manifest files for tz-aware
lengau Aug 8, 2024
9ea71d7
tests(data): fix expected yaml files parts properties
lengau Aug 8, 2024
77f5c7b
tests(application): improve equality tests.
lengau Aug 8, 2024
12fdd3b
fix(deps): fix requirements
lengau Aug 8, 2024
1b42bc3
chore: autoformat
lengau Aug 8, 2024
b38f6b8
fix: make bundles pack correctly
lengau Aug 8, 2024
c0a3bfa
Merge branch 'feature/pydantic-2' into work/1768/pydantic-2
lengau Aug 8, 2024
7103719
chore: use Python 3.12 on Windows
lengau Aug 8, 2024
29f22f9
ci: use built-in python on Linux, else installed
lengau Aug 8, 2024
d2097fc
build(deps): update dependencies for release
lengau Aug 9, 2024
f0d2786
style(lint): fix linting issues
lengau Aug 9, 2024
b74fcd0
ci: adjust tests for windows
lengau Aug 9, 2024
4249c32
ci: more fixes
lengau Aug 9, 2024
a9a3958
ci: enable noble
lengau Aug 9, 2024
52cd8cb
tests: expect failures on Windows with Python < 3.11
lengau Aug 9, 2024
70078b0
feat(deps)!: switch to pydantic 2 (#1777)
lengau Aug 9, 2024
2a0eb97
fix(yaml): correctly add default parts (#1815)
lengau Aug 15, 2024
69c9aeb
chore: merge branch 'main' into work/pydantic-2/merge-main
lengau Aug 15, 2024
f9d16ef
Merge branch 'main' into work/pydantic-2/merge-main
lengau Aug 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
ignoreDeps: [
// Each ignore is probably connected with an ignore in pyproject.toml.
// Ensure you change this and those simultaneously.
"pydantic", // Needs to wait for libraries.
"pydantic-yaml", // Needs to wait for pydantic
"urllib3",
"windows", // We'll update Windows versions manually.
],
Expand Down
59 changes: 39 additions & 20 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,52 +45,71 @@ jobs:
run-tests:
strategy:
matrix:
os: [ubuntu-22.04, macos-12, macos-13, windows-2019, windows-2022]
os: [ubuntu-22.04, ubuntu-24.04, macos-12, macos-13, windows-2019, windows-2022]
include:
- os: windows-2019
python-version: |
3.11
3.12
- os: windows-2022
python-version: |
3.11
3.12
- os: macos-12
python_version: |
3.10
3.12
- os: macos-13
python_version: |
3.10
3.12
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
if: ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: |
3.10
3.12
python-version: ${{ matrix.python-version }}
cache: "pip"
- name: Install Ubuntu-specific dependencies
if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt update
sudo apt install -y python3-pip python3-setuptools python3-wheel python3-venv libapt-pkg-dev
export $(cat /etc/os-release | grep VERSION_CODENAME)
pip install -U -r "requirements-${VERSION_CODENAME}.txt"
- name: Install external dependencies with homebrew
- name: Install skopeo (mac)
# This is only necessary for Linux until skopeo >= 1.11 is in repos.
# Once we're running on Noble, we can get skopeo from apt.
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}
if: ${{ runner.os == 'macOS' }}
run: |
if [[ $(uname --kernel-name) == "Linux" ]]; then
brew install skopeo
- name: Install skopeo (Linux)
if: ${{ runner.os == 'Linux' }}
run: |
if [[ $(cat /etc/os-release | grep VERSION_CODENAME) == 'VERSION_CODENAME=jammy' ]]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install skopeo
sudo rm -f /bin/skopeo
sudo ln -s $(which skopeo) /bin/skopeo
else
sudo apt install skopeo
fi
brew install skopeo
# Allow skopeo to access the contents of /run/containers
sudo chmod 777 /run/containers
# Add an xdg runtime dir for skopeo to look into for an auth.json file
sudo mkdir -p /run/user/$(id -u)
sudo chown $USER /run/user/$(id -u)
- name: Configure environment
run: |
python -m pip install tox
pipx install tox
tox run --colored yes -m tests --notest
- name: Run tests
shell: bash
run: |
if [[ $(uname --kernel-name) == "Linux" ]]; then
# Ensure the version of skopeo comes from homebrew
# This is only necessary until we move to noble.
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# Allow skopeo to access the contents of /run/containers
sudo chmod 777 /run/containers
# Add an xdg runtime dir for skopeo to look into for an auth.json file
sudo mkdir -p /run/user/$(id -u)
sudo chown $USER /run/user/$(id -u)
export XDG_RUNTIME_DIR=/run/user/$(id -u)
fi
tox run --skip-pkg-install --no-list-dependencies --result-json results/tox-${{ matrix.platform }}.json --colored yes -m tests
Expand Down Expand Up @@ -211,7 +230,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'
- name: Install dependencies
run: |
pip install -U pyinstaller -r requirements.txt
Expand Down
6 changes: 4 additions & 2 deletions charmcraft/application/commands/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,9 @@ def run(self, parsed_args: argparse.Namespace) -> None:
declared_libs = {lib.lib: lib for lib in charm_libs}
missing_store_libs = declared_libs.keys() - libs_metadata.keys()
if missing_store_libs:
missing_libs_source = [declared_libs[lib].dict() for lib in sorted(missing_store_libs)]
missing_libs_source = [
declared_libs[lib].model_dump() for lib in sorted(missing_store_libs)
]
libs_yaml = util.dump_yaml(missing_libs_source)
raise errors.CraftError(
f"Could not find the following libraries on charmhub:\n{libs_yaml}",
Expand Down Expand Up @@ -2205,7 +2207,7 @@ def run(self, parsed_args):
"revision": item.revision,
"created at": item.created_at.isoformat(),
"size": item.size,
"bases": [base.dict() for base in item.bases],
"bases": [base.model_dump() for base in item.bases],
}
for item in result
]
Expand Down
11 changes: 5 additions & 6 deletions charmcraft/models/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
import re

import pydantic
from craft_application.models import CraftBaseModel

from charmcraft.models.basic import ModelConfigDefaults


class JujuActions(ModelConfigDefaults, frozen=True):
class JujuActions(CraftBaseModel):
"""Juju actions for charms.

See also: https://juju.is/docs/sdk/actions
Expand All @@ -33,7 +32,7 @@ class JujuActions(ModelConfigDefaults, frozen=True):
_action_name_regex = re.compile(r"^[a-zA-Z_][a-zA-Z0-9-_]*$")
actions: dict[str, dict] | None

@pydantic.validator("actions")
@pydantic.field_validator("actions", mode="after")
def validate_actions(cls, actions):
"""Verify actions names and descriptions."""
if not isinstance(actions, dict):
Expand All @@ -48,8 +47,8 @@ def validate_actions(cls, actions):

return actions

@pydantic.validator("actions", each_item=True)
def validate_each_action(cls, action):
@pydantic.field_validator("actions", mode="after")
def _validate_actions(cls, action):
"""Verify actions names and descriptions."""
if not isinstance(action, dict):
raise TypeError(f"'{action}' is not a dictionary")
Expand Down
116 changes: 42 additions & 74 deletions charmcraft/models/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,80 +15,48 @@
# For further info, check https://github.com/canonical/charmcraft

"""Charmcraft basic pydantic model."""
import craft_application.models
import pydantic


class ModelConfigDefaults(
craft_application.models.CraftBaseModel,
frozen=True, # pyright: ignore[reportGeneralTypeIssues]
validate_all=True,
allow_population_by_field_name=False,
alias_generator=pydantic.BaseConfig.alias_generator,
):
"""Define Charmcraft's defaults for the BaseModel configuration."""


class CustomStrictStr(pydantic.StrictStr):
"""Generic class to create custom strict strings validated by pydantic."""

@classmethod
def __get_validators__(cls):
"""Yield the relevant validators."""
yield from super().__get_validators__()
yield cls.custom_validate


class RelativePath(CustomStrictStr):
"""Constrained string which must be a relative path."""

@classmethod
def custom_validate(cls, value: str) -> str:
"""Validate relative path.
from typing import Annotated

Check if it's an absolute path using POSIX's '/' (not os.path.sep, as the charm's
config is independent of the platform where charmcraft is running.
"""
if not value:
raise ValueError(f"{value!r} must be a valid relative path (cannot be empty)")

if value[0] == "/":
raise ValueError(f"{value!r} must be a valid relative path (cannot start with '/')")

return value


class AttributeName(CustomStrictStr):
"""Constrained string that must match the name of an attribute from linters.CHECKERS."""

@classmethod
def custom_validate(cls, value: str) -> str:
"""Validate attribute name."""
from charmcraft import linters # import here to avoid cyclic imports

valid_names = [
checker.name
for checker in linters.CHECKERS
if checker.check_type == linters.CheckType.ATTRIBUTE
]
if value not in valid_names:
raise ValueError(f"Bad attribute name {value!r}")
return value


class LinterName(CustomStrictStr):
"""Constrained string that must match the name of a linter from linters.CHECKERS."""
import craft_parts.constraints
import pydantic

@classmethod
def custom_validate(cls, value: str) -> str:
"""Validate attribute name."""
from charmcraft import linters # import here to avoid cyclic imports

valid_names = [
checker.name
for checker in linters.CHECKERS
if checker.check_type == linters.CheckType.LINT
]
if value not in valid_names:
raise ValueError(f"Bad lint name {value!r}")
return value
def _validate_attribute_name(value: str) -> str:
"""Validate attribute name."""
from charmcraft import linters # import here to avoid cyclic imports

valid_names = [
checker.name
for checker in linters.CHECKERS
if checker.check_type == linters.CheckType.ATTRIBUTE
]
if value not in valid_names:
raise ValueError(f"Bad attribute name {value!r}")
return value


def _validate_linter_name(value: str) -> str:
"""Validate linter name."""
from charmcraft import linters # import here to avoid cyclic imports

valid_names = [
checker.name
for checker in linters.CHECKERS
if checker.check_type == linters.CheckType.LINT
]
if value not in valid_names:
raise ValueError(f"Bad lint name {value!r}")
return value


RelativePath = craft_parts.constraints.RelativePathStr
AttributeName = Annotated[ # TODO: Turn this into a StrEnum
str,
pydantic.Field(strict=True),
pydantic.BeforeValidator(_validate_attribute_name),
]
LinterName = Annotated[
str,
pydantic.Field(strict=True),
pydantic.BeforeValidator(_validate_linter_name),
]
58 changes: 37 additions & 21 deletions charmcraft/models/charmcraft.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,41 @@
# For further info, check https://github.com/canonical/charmcraft

"""Charmcraft configuration pydantic model."""
from typing import cast
from typing import TypedDict, cast

import pydantic
from craft_application import util
from craft_application.models import CraftBaseModel
from typing_extensions import Self

from charmcraft.models.basic import AttributeName, LinterName, ModelConfigDefaults
from charmcraft.models.basic import AttributeName, LinterName


class CharmhubConfig(
ModelConfigDefaults,
alias_generator=lambda s: s.replace("_", "-"),
frozen=True,
):
class BaseDict(TypedDict, total=False):
"""TypedDict that describes only one base.

This is equivalent to the short form base definition.
"""

name: str
channel: str
architectures: list[str]


LongFormBasesDict = TypedDict(
"LongFormBasesDict", {"build-on": list[BaseDict], "run-on": list[BaseDict]}
)


class Charmhub(CraftBaseModel):
"""Definition of Charmhub endpoint configuration."""

api_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://api.charmhub.io")
storage_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://storage.snapcraftcontent.com")
registry_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://registry.jujucharms.com")


class Base(ModelConfigDefaults, frozen=True):
class Base(CraftBaseModel):
"""Represents a base."""

name: pydantic.StrictStr
Expand All @@ -54,11 +67,7 @@ def from_str_and_arch(cls, base_str: str, architectures: list[str]) -> Self:
return cls(name=name, channel=channel, architectures=architectures)


class BasesConfiguration(
ModelConfigDefaults,
alias_generator=lambda s: s.replace("_", "-"),
frozen=True,
):
class BasesConfiguration(CraftBaseModel):
"""Definition of build-on/run-on combinations.

Example::
Expand All @@ -80,30 +89,37 @@ class BasesConfiguration(
build_on: list[Base]
run_on: list[Base]

@pydantic.model_validator(mode="before")
def _expand_base(cls, base: BaseDict | LongFormBasesDict) -> LongFormBasesDict:
"""Expand short-form bases into long-form bases."""
if "build-on" in base: # Assume long-form base already.
return cast(LongFormBasesDict, base)
return cast(LongFormBasesDict, {"build-on": [base], "run-on": [base]})


class Ignore(ModelConfigDefaults, frozen=True):
class Ignore(CraftBaseModel):
"""Definition of `analysis.ignore` configuration."""

attributes: list[AttributeName] = []
linters: list[LinterName] = []


class AnalysisConfig(ModelConfigDefaults, allow_population_by_field_name=True, frozen=True):
class AnalysisConfig(CraftBaseModel):
"""Definition of `analysis` configuration."""

ignore: Ignore = Ignore()


class Links(ModelConfigDefaults, frozen=True):
class Links(CraftBaseModel):
"""Definition of `links` in metadata."""

contact: pydantic.StrictStr | list[pydantic.StrictStr] | None
contact: pydantic.StrictStr | list[pydantic.StrictStr] | None = None
"""Instructions for contacting the owner of the charm."""
documentation: pydantic.AnyHttpUrl | None
documentation: pydantic.AnyHttpUrl | None = None
"""The URL of the documentation for this charm."""
issues: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None
issues: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None = None
"""A link to the issue tracker for this charm."""
source: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None
source: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None = None
"""Where to find this charm's source code."""
website: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None
website: pydantic.AnyHttpUrl | list[pydantic.AnyHttpUrl] | None = None
"""The website for this charm."""
Loading
Loading