Skip to content

Commit 53046b5

Browse files
committed
Merge branch 'fix/pack_and_upload' into 'main'
Fix issues related to component uploads Closes PACMAN-961, PACMAN-958, PACMAN-962, PACMAN-957, and PACMAN-956 See merge request espressif/idf-component-manager!423
2 parents 233ac11 + f0af77f commit 53046b5

File tree

14 files changed

+547
-469
lines changed

14 files changed

+547
-469
lines changed

idf_component_manager/cli/component.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from .constants import (
77
get_dest_dir_option,
8+
get_name_option,
89
get_namespace_name_options,
910
get_project_dir_option,
1011
get_project_options,
@@ -15,6 +16,7 @@
1516
def init_component():
1617
PROJECT_DIR_OPTION = get_project_dir_option()
1718
PROJECT_OPTIONS = get_project_options()
19+
NAME_OPTION = get_name_option()
1820
NAMESPACE_NAME_OPTIONS = get_namespace_name_options()
1921
DEST_DIR_OPTION = get_dest_dir_option()
2022

@@ -55,14 +57,13 @@ def component():
5557
@component.command()
5658
@add_options(
5759
PROJECT_DIR_OPTION
58-
+ NAMESPACE_NAME_OPTIONS
60+
+ NAME_OPTION
5961
+ COMPONENT_VERSION_OPTION
6062
+ DEST_DIR_OPTION
6163
+ COMMIT_SHA_REPO_OPTION
6264
)
6365
def pack(
6466
manager,
65-
namespace,
6667
name,
6768
version,
6869
dest_dir,

idf_component_tools/config.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import os
77
import typing as t
8+
from pathlib import Path
89

910
import yaml
1011
from pydantic import (
@@ -26,8 +27,6 @@
2627

2728
from .build_system_tools import get_idf_version
2829

29-
DEFAULT_CONFIG_DIR = os.path.join('~', '.espressif')
30-
3130
RegistryUrlField = t.Union[
3231
Literal['default'],
3332
UrlField,
@@ -81,12 +80,12 @@ class Config(BaseModel):
8180
profiles: t.Dict[str, t.Optional[ProfileItem]] = {}
8281

8382

84-
def config_dir():
85-
return os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(DEFAULT_CONFIG_DIR)
83+
def config_dir() -> Path:
84+
return Path(os.environ.get('IDF_TOOLS_PATH') or Path.home() / '.espressif')
8685

8786

88-
def root_managed_components_dir():
89-
return os.path.join(config_dir(), 'root_managed_components', f'idf{get_idf_version()}')
87+
def root_managed_components_dir() -> Path:
88+
return config_dir() / 'root_managed_components' / f'idf{get_idf_version()}'
9089

9190

9291
class ConfigError(FatalError):
@@ -95,11 +94,11 @@ class ConfigError(FatalError):
9594

9695
class ConfigManager:
9796
def __init__(self, path=None):
98-
self.config_path = path or os.path.join(config_dir(), 'idf_component_manager.yml')
97+
self.config_path = Path(path) if path else (config_dir() / 'idf_component_manager.yml')
9998

10099
def load(self) -> Config:
101100
"""Loads config from disk"""
102-
if not os.path.isfile(self.config_path):
101+
if not self.config_path.is_file():
103102
return Config()
104103

105104
with open(self.config_path, encoding='utf-8') as f:
@@ -120,5 +119,9 @@ def validate(cls, data: t.Any) -> Config:
120119

121120
def dump(self, config: Config) -> None:
122121
"""Writes config to disk"""
122+
123+
# Make sure that directory exists
124+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
125+
123126
with open(self.config_path, mode='w', encoding='utf-8') as f:
124127
yaml.dump(data=config.model_dump(), stream=f, encoding='utf-8', allow_unicode=True)

idf_component_tools/manager.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import os
55
import typing as t
6+
from pathlib import Path
67

78
import yaml
89

@@ -30,7 +31,9 @@ def __init__(
3031
commit_sha: t.Optional[str] = None,
3132
repository_path: t.Optional[str] = None,
3233
) -> None:
33-
self.path = os.path.join(path, MANIFEST_FILENAME) if os.path.isdir(path) else path
34+
source_path = Path(path)
35+
self.path: Path = source_path / MANIFEST_FILENAME if source_path.is_dir() else source_path
36+
3437
self.name = name
3538

3639
self._manifest: 'Manifest' = None # type: ignore
@@ -48,30 +51,32 @@ def __init__(
4851
self._validation_errors: t.List[str] = None # type: ignore
4952

5053
def validate(self) -> 'ManifestManager':
51-
from .manifest.models import Manifest, RepositoryInfoField # avoid circular dependency
54+
from .manifest.models import (
55+
Manifest,
56+
RepositoryInfoField,
57+
)
58+
59+
# avoid circular dependency
5260
from .utils import ComponentVersion
5361

5462
if self._manifest:
5563
return self
5664

57-
if not os.path.isfile(self.path):
58-
self._validation_errors = []
59-
self._manifest = Manifest(name=self.name, manifest_manager=self)
60-
return self
61-
65+
if not self.path.exists():
66+
manifest_dict: t.Dict[str, t.Any] = {}
6267
# validate manifest
63-
try:
64-
with open(self.path, 'r') as f:
65-
d = yaml.safe_load(f) or {}
66-
except yaml.YAMLError:
67-
self._validation_errors = [
68-
'Cannot parse the manifest file. Please check that\n'
69-
'\t{}\n'
70-
'is a valid YAML file\n'.format(self.path)
71-
]
72-
return self
73-
74-
if not isinstance(d, dict):
68+
else:
69+
try:
70+
manifest_dict = yaml.safe_load(self.path.read_text()) or {}
71+
except yaml.YAMLError:
72+
self._validation_errors = [
73+
'Cannot parse the manifest file. Please check that\n'
74+
'\t{}\n'
75+
'is a valid YAML file\n'.format(self.path)
76+
]
77+
return self
78+
79+
if not isinstance(manifest_dict, dict):
7580
self._validation_errors = [
7681
'Manifest file should be a dictionary. Please check that\n'
7782
'\t{}\n'
@@ -80,15 +85,15 @@ def validate(self) -> 'ManifestManager':
8085
return self
8186

8287
if self.name:
83-
d['name'] = self.name
88+
manifest_dict['name'] = self.name
8489

8590
if self._version:
86-
d['version'] = self._version
91+
manifest_dict['version'] = self._version
8792

88-
d['manifest_manager'] = self
93+
manifest_dict['manifest_manager'] = self
8994

9095
self._validation_errors, self._manifest = Manifest.validate_manifest( # type: ignore
91-
d,
96+
manifest_dict,
9297
upload_mode=self.upload_mode,
9398
return_with_object=True,
9499
)
@@ -150,7 +155,7 @@ def load(self) -> 'Manifest':
150155

151156
def dump(
152157
self,
153-
path: t.Optional[str] = None,
158+
path: t.Optional[t.Union[str, Path]] = None,
154159
) -> None:
155160
if path is None:
156161
path = self.path

idf_component_tools/manifest/models.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from pydantic_core.core_schema import SerializerFunctionWrapHandler
1919
from pyparsing import ParseException
2020

21-
from idf_component_tools.build_system_tools import build_name, build_name_to_namespace_name
21+
from idf_component_tools.build_system_tools import (
22+
build_name,
23+
build_name_to_namespace_name,
24+
)
2225
from idf_component_tools.constants import (
2326
COMMIT_ID_RE,
2427
COMPILED_GIT_URL_RE,
@@ -386,7 +389,7 @@ def model_dump(
386389
self,
387390
**kwargs,
388391
) -> t.Dict[str, t.Any]:
389-
return super().model_dump(exclude=['name'])
392+
return super().model_dump(exclude=['name'], exclude_unset=True)
390393

391394
@field_validator('version')
392395
@classmethod
@@ -566,7 +569,7 @@ def real_name(self) -> str:
566569

567570
@property
568571
def path(self) -> str:
569-
return self._manifest_manager.path if self._manifest_manager else ''
572+
return str(self._manifest_manager.path) if self._manifest_manager else ''
570573

571574

572575
class SolvedComponent(BaseModel):

tests/cli/test_compote.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,17 @@ def test_raise_exception_on_warnings(monkeypatch):
4747
)
4848

4949

50-
def test_login_to_registry(monkeypatch, tmp_path, mock_registry, mock_token_information):
51-
monkeypatch.setenv('IDF_TOOLS_PATH', str(tmp_path))
52-
50+
def test_login_to_registry(tmp_path, mock_registry, mock_token_information):
5351
runner = CliRunner()
5452
cli = initialize_cli()
5553
output = runner.invoke(
5654
cli,
5755
['registry', 'login', '--no-browser'],
5856
input='test_token',
59-
env={'IDF_TOOLS_PATH': str(tmp_path)},
57+
env={
58+
# non-existing path is to check PACMAN-961
59+
'IDF_TOOLS_PATH': str(tmp_path / 'non-existing-path')
60+
},
6061
)
6162

6263
assert output.exit_code == 0

tests/core/test_add_dependency.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test Core commands"""
4+
5+
import pytest
6+
import vcr
7+
8+
from idf_component_manager.core import ComponentManager
9+
from idf_component_tools.constants import MANIFEST_FILENAME
10+
from idf_component_tools.errors import FatalError
11+
from idf_component_tools.manager import ManifestManager
12+
13+
14+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_init_project.yaml')
15+
def test_init_project(mock_registry, tmp_path):
16+
(tmp_path / 'main').mkdir()
17+
(tmp_path / 'components' / 'foo').mkdir(parents=True)
18+
main_manifest_path = tmp_path / 'main' / MANIFEST_FILENAME
19+
foo_manifest_path = tmp_path / 'components' / 'foo' / MANIFEST_FILENAME
20+
21+
manager = ComponentManager(path=str(tmp_path))
22+
manager.create_manifest()
23+
manager.create_manifest(component='foo')
24+
25+
for filepath in [main_manifest_path, foo_manifest_path]:
26+
assert filepath.read_text().startswith('## IDF Component Manager')
27+
28+
manager.add_dependency('cmp==4.0.3')
29+
manifest_manager = ManifestManager(str(main_manifest_path), 'main')
30+
assert manifest_manager.manifest_tree['dependencies']['espressif/cmp'] == '==4.0.3'
31+
32+
manager.add_dependency('espressif/cmp==4.0.3', component='foo')
33+
manifest_manager = ManifestManager(str(foo_manifest_path), 'foo')
34+
assert manifest_manager.manifest_tree['dependencies']['espressif/cmp'] == '==4.0.3'
35+
36+
37+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_init_project_with_path.yaml')
38+
def test_init_project_with_path(mock_registry, tmp_path):
39+
src_path = tmp_path / 'src'
40+
src_path.mkdir(parents=True, exist_ok=True)
41+
src_manifest_path = src_path / MANIFEST_FILENAME
42+
43+
outside_project_path = tmp_path.parent
44+
outside_project_path_error_match = 'Directory ".*" is not under project directory!'
45+
component_and_path_error_match = 'Cannot determine manifest directory.'
46+
47+
manager = ComponentManager(path=str(tmp_path))
48+
manager.create_manifest(path=str(src_path))
49+
50+
with pytest.raises(FatalError, match=outside_project_path_error_match):
51+
manager.create_manifest(path=str(outside_project_path))
52+
53+
with pytest.raises(FatalError, match=component_and_path_error_match):
54+
manager.create_manifest(component='src', path=str(src_path))
55+
56+
manager.add_dependency('espressif/cmp==4.0.3', path=str(src_path))
57+
manifest_manager = ManifestManager(str(src_manifest_path), 'src')
58+
59+
assert manifest_manager.manifest_tree['dependencies']['espressif/cmp'] == '==4.0.3'
60+
61+
with pytest.raises(FatalError, match=outside_project_path_error_match):
62+
manager.create_manifest(path=str(outside_project_path))
63+
64+
with pytest.raises(FatalError, match=component_and_path_error_match):
65+
manager.add_dependency('espressif/cmp==4.0.3', component='src', path=str(src_path))
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
import vcr
4+
from pytest import raises
5+
6+
from idf_component_manager.core import ComponentManager
7+
from idf_component_tools.errors import FatalError
8+
9+
10+
def test_create_example_project_path_not_a_directory(tmp_path):
11+
existing_file = tmp_path / 'example'
12+
existing_file.write_text('test')
13+
14+
manager = ComponentManager(path=str(tmp_path))
15+
16+
with raises(FatalError, match='Your target path is not a directory*'):
17+
manager.create_project_from_example('test:example')
18+
19+
20+
def test_create_example_project_path_not_empty(tmp_path):
21+
example_dir = tmp_path / 'example'
22+
example_dir.mkdir()
23+
existing_file = example_dir / 'test'
24+
existing_file.write_text('test')
25+
26+
manager = ComponentManager(path=str(tmp_path))
27+
28+
with raises(FatalError, match='To create an example you must*'):
29+
manager.create_project_from_example('test:example')
30+
31+
32+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_create_example_component_not_exist.yaml')
33+
def test_create_example_component_not_exist(tmp_path):
34+
manager = ComponentManager(path=str(tmp_path))
35+
with raises(FatalError, match='Component "espressif/test" not found'):
36+
manager.create_project_from_example('test:example')
37+
38+
39+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_create_example_not_exist.yaml')
40+
def test_create_example_version_not_exist(mock_registry, tmp_path):
41+
manager = ComponentManager(path=str(tmp_path))
42+
with raises(
43+
FatalError,
44+
match='Version of the component "test/cmp" satisfying the spec "=2.0.0" was not found.',
45+
):
46+
manager.create_project_from_example('test/cmp=2.0.0:example')
47+
48+
49+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_create_example_not_exist.yaml')
50+
def test_create_example_not_exist(mock_registry, tmp_path):
51+
manager = ComponentManager(path=str(tmp_path))
52+
with raises(
53+
FatalError,
54+
match='Cannot find example "example" for "test/cmp" version "=1.0.1"',
55+
):
56+
manager.create_project_from_example('test/cmp=1.0.1:example')
57+
58+
59+
@vcr.use_cassette('tests/fixtures/vcr_cassettes/test_create_example_success.yaml')
60+
def test_create_example_success(mock_registry, tmp_path):
61+
manager = ComponentManager(path=str(tmp_path))
62+
manager.create_project_from_example('test/cmp>=1.0.0:sample_project')

0 commit comments

Comments
 (0)