diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index e9dd6c8..e5d1c37 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,10 +1,16 @@ -## Release 0.4.0 (development release) +## Release 0.3.2 (current release) + +### Bug fixes + +* Sets lower bound on compatible `pydantic` versions to v2.0.0. [(#46)](https://github.com/XanaduAI/xanadu-cloud-client/pull/46) ### Contributors This release contains contributions from (in alphabetical order): -## Release 0.3.1 (current release) +[Mikhail Andrenkov](https://github.com/Mandrenkov), [Luke Helt](https://github.com/heltluke). + +## Release 0.3.1 ### Improvements diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 7923238..2b29f36 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: "3.8" - uses: actions/checkout@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78e3b52..d577477 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: ["3.8", "3.9", "3.10"] steps: - name: Cancel Previous Runs diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index 49a9aca..d9c9504 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: "3.8" - name: Install dependencies run: | diff --git a/requirements.txt b/requirements.txt index c43412b..f78ce53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ appdirs==1.4.4 fire==0.4.0 numpy==1.21.3 -pydantic[dotenv]==1.8.2 +pydantic==2.4 +pydantic-settings==2.1.0 python-dateutil==2.8.2 +python-dotenv==0.21.1 requests==2.31.0 diff --git a/setup.py b/setup.py index d603e1f..10b106a 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,10 @@ "appdirs", "fire", "numpy", - "pydantic[dotenv]<2", + "pydantic>=2", + "pydantic-settings", "python-dateutil", + "python-dotenv", "requests", ] @@ -41,7 +43,6 @@ "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/tests/conftest.py b/tests/conftest.py index 95cd8df..7e5d91c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,14 +19,14 @@ def connection() -> xcc.Connection: def settings(monkeypatch) -> Iterator[xcc.Settings]: """Returns a :class:`xcc.Settings` instance configured to use a mock .env file.""" with NamedTemporaryFile("w") as env_file: - monkeypatch.setattr("xcc.Settings.Config.env_file", env_file.name) + monkeypatch.setitem(xcc.Settings.model_config, "env_file", env_file.name) settings_ = xcc.Settings(REFRESH_TOKEN="j.w.t", HOST="example.com", PORT=80, TLS=False) # Saving ensures that new Settings instances are loaded with the same values. settings_.save() # Environment variables take precedence over fields in the .env file. - for env_var in map(xcc.settings.get_name_of_env_var, settings_.dict()): + for env_var in map(xcc.settings.get_name_of_env_var, settings_.model_dump()): monkeypatch.delenv(env_var, raising=False) yield settings_ diff --git a/tests/test_commands.py b/tests/test_commands.py index d2b429f..f20f758 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -179,7 +179,10 @@ def test_invalid_name(self): def test_invalid_value(self): """Tests that a ValueError is raised when the value of a setting is invalid.""" - match = r"Failed to update PORT setting: value is not a valid integer" + match = ( + r"Failed to update PORT setting: " + r"Input should be a valid integer, unable to parse string as an integer" + ) with pytest.raises(ValueError, match=match): xcc.commands.set_setting(name="PORT", value="string") diff --git a/tests/test_settings.py b/tests/test_settings.py index 52f54ba..118033b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -15,7 +15,7 @@ def env_file(monkeypatch): """Returns a mock .env file which :class:`xcc.Settings` is configured to use.""" with NamedTemporaryFile("w") as env_file: - monkeypatch.setattr("xcc.settings.Settings.Config.env_file", env_file.name) + monkeypatch.setitem(xcc.Settings.model_config, "env_file", env_file.name) yield env_file @@ -77,7 +77,7 @@ def test_save_bad_base64_url(self, settings): settings.save() # Check that the .env file was not modified since there was a "\n" in the refresh token. - assert dotenv_values(xcc.Settings.Config.env_file) == { + assert dotenv_values(settings.model_config["env_file"]) == { "XANADU_CLOUD_REFRESH_TOKEN": "j.w.t", "XANADU_CLOUD_HOST": "example.com", "XANADU_CLOUD_PORT": "80", @@ -86,7 +86,7 @@ def test_save_bad_base64_url(self, settings): def test_save_multiple_times(self, settings): """Tests that settings can be saved to a .env file multiple times.""" - path_to_env_file = xcc.Settings.Config.env_file + path_to_env_file = settings.model_config["env_file"] settings.REFRESH_TOKEN = None settings.save() @@ -108,7 +108,7 @@ def test_save_to_nonexistent_directory(self, monkeypatch): """Tests that settings can be saved to a .env file in a nonexistent directory.""" with TemporaryDirectory() as env_dir: env_file = os.path.join(env_dir, "foo", "bar", ".env") - monkeypatch.setattr("xcc.settings.Settings.Config.env_file", env_file) + monkeypatch.setitem(xcc.Settings.model_config, "env_file", env_file) xcc.Settings().save() assert os.path.exists(env_file) is True diff --git a/xcc/_version.py b/xcc/_version.py index 28cd148..0519186 100644 --- a/xcc/_version.py +++ b/xcc/_version.py @@ -3,4 +3,4 @@ See https://semver.org/. """ -__version__ = "0.4.0-dev" +__version__ = "0.3.2" diff --git a/xcc/commands.py b/xcc/commands.py index 88406b2..9a01916 100644 --- a/xcc/commands.py +++ b/xcc/commands.py @@ -87,7 +87,7 @@ def list_settings() -> Mapping[str, Any]: Returns: Mapping[str, Any]: Mapping from setting names to values. """ - return Settings().dict() + return Settings().model_dump() @beautify @@ -108,14 +108,14 @@ def set_setting(name: str, value: Union[str, int, bool]) -> str: key, _ = _resolve_setting(name) try: - settings = Settings(**{key: value}) + settings = Settings.model_validate({key: value}) settings.save() except ValidationError as exc: err = exc.errors()[0].get("msg", "invalid value") raise ValueError(f"Failed to update {key} setting: {err}") from exc # Using repr() ensures that strings are quoted. - val = repr(settings.dict()[key]) + val = repr(settings.model_dump()[key]) return f"Successfully updated {key} setting to {val}." @@ -133,11 +133,11 @@ def _resolve_setting(name: str) -> Tuple[str, Any]: """ key = name.upper() - settings = Settings() - if key not in settings.dict(): - raise ValueError(f"The setting name '{name}' must be one of {list(settings.dict())}.") + settings_dict = Settings().model_dump() + if key not in settings_dict: + raise ValueError(f"The setting name '{name}' must be one of {list(settings_dict)}.") - return key, settings.dict()[key] + return key, settings_dict[key] # Device CLI diff --git a/xcc/settings.py b/xcc/settings.py index 15905d8..6eed19e 100644 --- a/xcc/settings.py +++ b/xcc/settings.py @@ -8,7 +8,7 @@ from appdirs import user_config_dir from dotenv import dotenv_values, set_key, unset_key -from pydantic import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict # Matches when string contains chars outside Base64URL set # https://base64.guru/standards/base64url @@ -69,7 +69,7 @@ class Settings(BaseSettings): >>> import xcc >>> settings = xcc.Settings() >>> settings - REFRESH_TOKEN=None ACCESS_TOKEN=None HOST='platform.xanadu.ai' PORT=443 TLS=True + Settings(REFRESH_TOKEN=None, ACCESS_TOKEN=None, HOST'platform.xanadu.ai', PORT=443, TLS=True) Now, individual options can be accessed or assigned through their corresponding attribute: @@ -84,9 +84,9 @@ class Settings(BaseSettings): Several aggregate representations of options are also available, such as - >>> settings.dict() + >>> settings.model_dump() {'REFRESH_TOKEN': None, 'ACCESS_TOKEN': None, ..., 'TLS': True} - >>> settings.json() + >>> settings.model_dump_json() '{"REFRESH_TOKEN": null, "ACCESS_TOKEN": null, ..., "TLS": true}' Finally, saving a configuration can be done by invoking :meth:`Settings.save`: @@ -109,25 +109,26 @@ class Settings(BaseSettings): TLS: bool = True """Whether to use HTTPS for requests to the Xanadu Cloud.""" - class Config: # pylint: disable=missing-class-docstring - case_sensitive = True - env_file = get_path_to_env_file() - env_prefix = get_name_of_env_var() + model_config = SettingsConfigDict( + case_sensitive=True, + env_file=get_path_to_env_file(), + env_prefix=get_name_of_env_var(), + ) def save(self) -> None: """Saves the current settings to the .env file.""" - env_file = Settings.Config.env_file + env_file = self.model_config["env_file"] env_dir = os.path.dirname(env_file) os.makedirs(env_dir, exist_ok=True) saved = dotenv_values(dotenv_path=env_file) - # must be done first as dict is not ordered - for key, val in self.dict().items(): + # Must be done first as the dictionary is not ordered. + for key, val in self.model_dump().items(): _check_for_invalid_values(key, val) - for key, val in self.dict().items(): + for key, val in self.model_dump().items(): field = get_name_of_env_var(key) # Remove keys that are assigned to None.