diff --git a/src/nskit/vcs/codebase.py b/src/nskit/vcs/codebase.py index c3ac3eb..869ca03 100644 --- a/src/nskit/vcs/codebase.py +++ b/src/nskit/vcs/codebase.py @@ -22,15 +22,19 @@ class Codebase(BaseConfiguration): """Object for managing a codebase.""" root_dir: Path = Field(default_factory=Path.cwd) - settings: Annotated[CodebaseSettings, Field(validate_default=True)] = None namespaces_dir: Path = Path('.namespaces') + settings: Annotated[CodebaseSettings, Field(validate_default=True)] = None namespace_validation_repo: Optional[NamespaceValidationRepo] = None @field_validator('settings', mode='before') @classmethod - def _validate_settings(cls, value): + def _validate_settings(cls, value, info: ValidationInfo): if value is None: - value = CodebaseSettings() + namespace_validation_repo = None + if (info.data.get('root_dir')/info.data.get('namespaces_dir')).exists(): + # Namespaces repo exists + namespace_validation_repo = NamespaceValidationRepo(local_dir=info.data.get('root_dir')/info.data.get('namespaces_dir')) + value = CodebaseSettings(namespace_validation_repo=namespace_validation_repo) return value @field_validator('namespace_validation_repo', mode='before') @@ -157,7 +161,7 @@ def create_namespace_repo( self.namespace_validation_repo = NamespaceValidationRepo( name=name, namespaces_filename=namespaces_filename, - local_dir=self.namespaces_dir + local_dir=self.root_dir/self.namespaces_dir ) self.namespace_validation_repo.create( namespace_options=namespace_options, diff --git a/src/nskit/vcs/namespace_validator.py b/src/nskit/vcs/namespace_validator.py index 9ba0985..dfe77d9 100644 --- a/src/nskit/vcs/namespace_validator.py +++ b/src/nskit/vcs/namespace_validator.py @@ -21,9 +21,9 @@ class ValidationEnum(Enum): """Enum for validation level.""" - strict = 2 - warn = 1 - none = 0 + strict = '2' + warn = '1' + none = '0' class NamespaceValidator(BaseConfiguration): diff --git a/src/nskit/vcs/providers/github.py b/src/nskit/vcs/providers/github.py index 6fac67b..a49ee8e 100644 --- a/src/nskit/vcs/providers/github.py +++ b/src/nskit/vcs/providers/github.py @@ -17,14 +17,8 @@ class GithubRepoSettings(BaseConfiguration): """Github Repo settings.""" + model_config = SettingsConfigDict(env_prefix='GITHUB_REPO', env_file='.env') - # # This is not ideal behaviour, but due to the issue highlighted in - # # https://github.com/pydantic/pydantic-settings/issues/245 and the - # # non-semver compliant versioning in pydantic-settings, we need to add this behaviour - # # this now changes the API behaviour for these objects as they will - # # also ignore additional inputs in the python initialisation - # # We will pin to version < 2.1.0 instead of allowing 2.2.0+ as it requires the code below: - # model_config = ConfigDict(extra='ignore') noqa: E800 private: bool = True has_issues: Optional[bool] = None has_wiki: Optional[bool] = None diff --git a/src/nskit/vcs/repo.py b/src/nskit/vcs/repo.py index 07aaa7c..cf98dc8 100644 --- a/src/nskit/vcs/repo.py +++ b/src/nskit/vcs/repo.py @@ -260,7 +260,7 @@ def validate_name(self, proposed_name: str): @classmethod def _validate_local_dir(cls, value: Any, info: ValidationInfo): if value is None: - value = Path(tempfile.tempdir)/info.data['name'] + value = Path(tempfile.gettempdir())/info.data['name'] return value def _download_namespaces(self): diff --git a/src/nskit/vcs/settings.py b/src/nskit/vcs/settings.py index e830100..47d4ebf 100644 --- a/src/nskit/vcs/settings.py +++ b/src/nskit/vcs/settings.py @@ -1,15 +1,16 @@ """Codebase Settings.""" from __future__ import annotations +from pathlib import Path import sys -from typing import Optional +from typing import Optional, Union if sys.version_info.major <= 3 and sys.version_info.minor <= 8: from typing_extensions import Annotated else: from typing import Annotated -from pydantic import Field, field_validator, ValidationError +from pydantic import Field, field_validator, model_validator, ValidationError from pydantic_settings import SettingsConfigDict from nskit._logging import logger_factory @@ -34,7 +35,7 @@ class CodebaseSettings(BaseConfiguration): default_branch: str = 'main' vcs_provider: Annotated[ProviderEnum, Field(validate_default=True)] = None - namespace_validation_repo: Optional[NamespaceValidationRepo] = None + namespace_validation_repo: Optional[Union[NamespaceValidationRepo, str]] = None validation_level: ValidationEnum = ValidationEnum.none _provider_settings = None @@ -59,6 +60,17 @@ def _validate_vcs_provider(cls, value): raise ValueError('Extension Not Found') return value + @model_validator(mode='after') + def _validate_namespace_validation_repo_default(self): + if self.namespace_validation_repo is None and Path('.namespaces').exists(): + # Can we handle the root pathing + self.namespace_validation_repo = '.namespaces' + if isinstance(self.namespace_validation_repo, str): + self.namespace_validation_repo = NamespaceValidationRepo(name=self.namespace_validation_repo, + local_dir=Path(self.namespace_validation_repo), + provider_client=self.provider_settings.repo_client) + return self + @property def provider_settings(self) -> VCSProviderSettings: """Get the instantiated provider settings.""" diff --git a/tests/unit/test_vcs/test_codebase.py b/tests/unit/test_vcs/test_codebase.py index a3ac128..2269497 100644 --- a/tests/unit/test_vcs/test_codebase.py +++ b/tests/unit/test_vcs/test_codebase.py @@ -1,5 +1,5 @@ from pathlib import Path -import sys +import tempfile import unittest from unittest.mock import call, create_autospec, MagicMock, patch @@ -33,8 +33,9 @@ def repo_client(self2): def extension(self): return TestExtension('dummy', ENTRYPOINT, self._provider_settings_cls) - def env(self): - return Env(override={'TEST_ABACUS': 'A'}) + def env(self, **override): + override.update({'TEST_ABACUS': 'A'}) + return Env(override=override) @patch.object(RepoClient, '__abstractmethods__', set()) def test_init_no_settings_error(self): @@ -106,6 +107,31 @@ def test_validation_namespace_local_dir(self): self.assertEqual(c.namespace_validation_repo.name, 'abc') self.assertEqual(c.namespace_validation_repo.local_dir, Path.cwd()/'a'/'.namespaces2') + @patch.object(RepoClient, '__abstractmethods__', set()) + def test_init_namespace_validation_repo_from_env_default(self): + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + Path('.namespaces23').mkdir(parents=True) + nsv = NamespaceValidator(options=[{'a': ['b', 'c']}]) + Path('.namespaces23/namespaces.yaml').write_text(nsv.model_dump_yaml()) + with self.extension(): + with self.env(NSKIT_VCS_CODEBASE_NAMESPACES_DIR='.namespaces23'): + settings.ProviderEnum._patch() + c = Codebase(namespaces_dir='.namespaces23') + self.assertIsInstance(c.namespace_validation_repo, NamespaceValidationRepo) + self.assertEqual(c.namespace_validation_repo.name, '.namespaces') + self.assertEqual(c.namespace_validation_repo.local_dir.name, '.namespaces23') + + @patch.object(RepoClient, '__abstractmethods__', set()) + def test_init_namespace_validation_repo_from_env_missing(self): + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + with self.extension(): + with self.env(NSKIT_VCS_CODEBASE_NAMESPACES_DIR='.namespaces'): + settings.ProviderEnum._patch() + c = Codebase() + self.assertIsNone(c.namespace_validation_repo, NamespaceValidationRepo) + @patch.object(RepoClient, '__abstractmethods__', set()) def test_namespace_validator_provided(self): r = NamespaceValidationRepo(name='abc', provider_client=RepoClient()) @@ -445,7 +471,7 @@ def test_create_namespace_repo_name(self, nsv_rp): options = [{'a': ['b', 'c']}, 'd'] c.create_namespace_repo(name='test-namespaces', namespace_options=options) self.assertIsNotNone(c.namespace_validation_repo) - nsv_rp.assert_called_once_with(name='test-namespaces', namespaces_filename='namespaces.yaml', local_dir=Path('.namespaces')) + nsv_rp.assert_called_once_with(name='test-namespaces', namespaces_filename='namespaces.yaml', local_dir=c.root_dir/'.namespaces') c.namespace_validation_repo.create.assert_called_once_with(namespace_options=options, delimiters=None, repo_separator=None) @patch.object(codebase, 'NamespaceValidationRepo', autospec=True) @@ -460,6 +486,6 @@ def test_create_namespace_repo_no_name(self, nsv_rp): options = [{'a': ['b', 'c']}, 'd'] c.create_namespace_repo(namespace_options=options) self.assertIsNotNone(c.namespace_validation_repo) - nsv_rp.assert_called_once_with(name='.namespaces2', namespaces_filename='namespaces.yaml', local_dir=Path('.namespaces2')) + nsv_rp.assert_called_once_with(name='.namespaces2', namespaces_filename='namespaces.yaml', local_dir=c.root_dir/'.namespaces2') c.namespace_validation_repo.create.assert_called_once_with(namespace_options=options, delimiters=None, repo_separator=None) diff --git a/tests/unit/test_vcs/test_settings.py b/tests/unit/test_vcs/test_settings.py index 00211b5..d1f4e4f 100644 --- a/tests/unit/test_vcs/test_settings.py +++ b/tests/unit/test_vcs/test_settings.py @@ -1,7 +1,9 @@ from __future__ import annotations +from pathlib import Path import sys import unittest +from unittest.mock import call, create_autospec, MagicMock, patch import uuid if sys.version_info.major >= 3 and sys.version_info.minor >= 9: @@ -12,16 +14,49 @@ from pydantic import Field, ValidationError from pydantic_settings import SettingsConfigDict -from nskit.common.contextmanagers import Env, TestExtension +from nskit.common.contextmanagers import ChDir, Env, TestExtension from nskit.common.extensions import ExtensionsEnum from nskit.vcs import settings +from nskit.vcs.namespace_validator import NamespaceValidator from nskit.vcs.providers import ENTRYPOINT from nskit.vcs.providers.abstract import VCSProviderSettings +from nskit.vcs.repo import NamespaceValidationRepo, RepoClient from nskit.vcs.settings import CodebaseSettings class VCSSettingsTestCase(unittest.TestCase): + def setUp(self): + + self._MockedRepoClientKls = create_autospec(RepoClient) + self._mocked_repo_client = self._MockedRepoClientKls() + + class DummyVCSProviderSettings(VCSProviderSettings): + + test_abacus: str + + @property + def repo_client(self2): + return self._mocked_repo_client + + self._provider_settings_cls = DummyVCSProviderSettings + self._entrypoint = f'nskit.vcs.providers.e{uuid.uuid4()}' + + def extension(self, solo=True): + return TestExtension('dummy', self._entrypoint, self._provider_settings_cls, solo=solo) + + def patched_settings(self): + PatchedProviderEnum = ExtensionsEnum.from_entrypoint('PatchedProviderEnum', self._entrypoint) + + class PatchedSettings(CodebaseSettings): + model_config = SettingsConfigDict(env_file=None) + vcs_provider: Annotated[PatchedProviderEnum, Field(validate_default=True)] = None + + return PatchedSettings + + def env(self, **override): + override.update({'TEST_ABACUS': 'A'}) + return Env(override=override) def test_default_init_failed(self): @@ -51,6 +86,61 @@ class PatchedSettings(CodebaseSettings): with self.assertRaises(ValidationError): PatchedSettings(model_config=dict(env_file=f'{uuid.uuid4()}.env')) + def test_namespaces_validation_repo_from_str(self): + + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + Path('.namespaces23').mkdir(parents=True) + nsv = NamespaceValidator(options=[{'a': ['b', 'c']}]) + Path('.namespaces23/namespaces.yaml').write_text(nsv.model_dump_yaml()) + with self.extension(): + PatchedSettings = self.patched_settings() + with Env(override={'TEST_ABACUS': 'A'}): + s = PatchedSettings(namespace_validation_repo='.namespaces23') + self.assertIsInstance(s.namespace_validation_repo, NamespaceValidationRepo) + self.assertEqual(s.namespace_validation_repo.local_dir.name, '.namespaces23') + + def test_namespaces_validation_repo_from_env(self): + + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + Path('.namespaces23').mkdir(parents=True) + nsv = NamespaceValidator(options=[{'a': ['b', 'c']}]) + Path('.namespaces23/namespaces.yaml').write_text(nsv.model_dump_yaml()) + with self.extension(): + PatchedSettings = self.patched_settings() + with Env(override={'TEST_ABACUS': 'A', 'NSKIT_VCS_CODEBASE_NAMESPACE_VALIDATION_REPO': '.namespaces23'}): + s = PatchedSettings() + self.assertIsInstance(s.namespace_validation_repo, NamespaceValidationRepo) + self.assertEqual(s.namespace_validation_repo.local_dir.name, '.namespaces23') + + def test_namespaces_validation_repo_not_exists(self): + + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + Path('.namespaces23').mkdir(parents=True) + nsv = NamespaceValidator(options=[{'a': ['b', 'c']}]) + Path('.namespaces23/namespaces.yaml').write_text(nsv.model_dump_yaml()) + with self.extension(): + PatchedSettings = self.patched_settings() + with Env(override={'TEST_ABACUS': 'A'}): + s = PatchedSettings() + self.assertIsNone(s.namespace_validation_repo) + + def test_namespaces_validation_repo_from_default(self): + + with ChDir(): # Make sure theres no .env file when running tests + # Create the namespaces dir + Path('.namespaces').mkdir(parents=True) + nsv = NamespaceValidator(options=[{'a': ['b', 'c']}]) + Path('.namespaces/namespaces.yaml').write_text(nsv.model_dump_yaml()) + with self.extension(): + PatchedSettings = self.patched_settings() + with Env(override={'TEST_ABACUS': 'A'}): + s = PatchedSettings() + self.assertIsInstance(s.namespace_validation_repo, NamespaceValidationRepo) + self.assertEqual(s.namespace_validation_repo.local_dir.name, '.namespaces') + def test_default_init_test_with_valid_provider_found(self):