Skip to content

Commit f6e42d2

Browse files
committed
qol changes, use pydantic, fix #146
1 parent 97856a8 commit f6e42d2

10 files changed

Lines changed: 214 additions & 164 deletions

File tree

licensecheck/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from licensecheck.models.constants import JOINS
99
from licensecheck.models.license import License
1010
from licensecheck.models.packageinfo import PackageInfo
11-
from licensecheck.packageinfo import PackageInfoManager
11+
from licensecheck.packageinforesolver import PackageInfoManager
1212

1313

1414
def check(

licensecheck/io/cli.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from configurator import Config
1212
from configurator.node import ConfigNode
1313

14-
from licensecheck import checker, license_matrix, packageinfo
14+
from licensecheck import checker, license_matrix
1515
from licensecheck.io import fmt
1616
from licensecheck.models.config import LC_Config
1717
from licensecheck.models.packageinfo import PackageInfo
18+
from licensecheck.packageinforesolver import PackageInfoManager, ProjectMetadata
1819

1920
stdout.reconfigure(encoding="utf-8") # type: ignore[general-type-issues]
2021

@@ -117,23 +118,20 @@ def cli() -> None: # pragma: no cover
117118

118119
config: ConfigNode = Config()
119120

120-
# (Parses in the following order: `pyproject.toml`,
121-
# `setup.cfg`, `licensecheck.toml`, `licensecheck.json`,
122-
# `licensecheck.ini`, `~/licensecheck.toml`, `~/licensecheck.json`, `~/licensecheck.ini`)
121+
# (Parses in the following order:
123122
config_files = [
124123
"~/licensecheck.json",
125124
"~/licensecheck.toml",
126125
"licensecheck.json",
127126
"licensecheck.toml",
128-
"setup.cfg",
129127
"pyproject.toml",
130128
]
131129

132130
for file in config_files:
133131
config += Config.from_path(file, optional=True)
134132

135133
scopedData: ConfigNode = config.get("tool", {}).get("licensecheck", ConfigNode())
136-
licensecheckConf: LC_Config = LC_Config.from_mapping(**{**scopedData.data, **args})
134+
licensecheckConf: LC_Config = LC_Config.model_validate({**scopedData.data, **args})
137135

138136
ec = main(licensecheckConf)
139137
stdin_path.unlink(missing_ok=True)
@@ -149,17 +147,15 @@ def main(licensecheckConf: LC_Config) -> int:
149147
requirements_paths = licensecheckConf.requirements_paths or {"__stdin__"}
150148
output_file = (
151149
stdout
152-
if licensecheckConf.file in [None, ""]
150+
if licensecheckConf.file == ""
153151
else Path(licensecheckConf.file or "").open("w", encoding="utf-8")
154152
)
155153

156154
# Get my license
157-
this_license_text = licensecheckConf.license or packageinfo.ProjectMetadata.get_license()
155+
this_license_text = licensecheckConf.license or ProjectMetadata.get_license()
158156
this_license = license_matrix.licenseType(this_license_text).pop()
159157

160-
package_info_manager = packageinfo.PackageInfoManager(
161-
licensecheckConf.pypi_api or "https://pypi.org"
162-
)
158+
package_info_manager = PackageInfoManager(licensecheckConf.pypi_api or "https://pypi.org")
163159

164160
incompatible, depsWithLicenses = checker.check(
165161
requirements_paths=set(requirements_paths),
@@ -206,6 +202,6 @@ def main(licensecheckConf: LC_Config) -> int:
206202
exitCode = 1
207203

208204
# Cleanup + exit
209-
if licensecheckConf.file not in [None, ""]:
205+
if licensecheckConf.file != "":
210206
output_file.close()
211207
return exitCode

licensecheck/models/config.py

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass, field
4-
from typing import Any
3+
from dataclasses import field
54

5+
from licensecheck.models.defaultonnone import DefaultOnNoneModel
66

7-
@dataclass(unsafe_hash=True, order=True)
8-
class LC_Config:
7+
8+
class LC_Config(DefaultOnNoneModel):
99
"""LC_Config type."""
1010

11-
file: str | None
12-
license: str | None
13-
format: str | None
14-
pypi_api: str | None
15-
show_only_failing: bool
16-
zero: bool
11+
file: str = ""
12+
license: str = ""
13+
format: str = ""
14+
pypi_api: str = ""
15+
show_only_failing: bool = False
16+
zero: bool = False
1717

1818
requirements_paths: set[str] = field(default_factory=set)
1919
groups: set[str] = field(default_factory=set)
@@ -25,19 +25,3 @@ class LC_Config:
2525
only_licenses: set[str] = field(default_factory=set)
2626
skip_dependencies: set[str] = field(default_factory=set)
2727
hide_output_parameters: set[str] = field(default_factory=set)
28-
29-
@staticmethod
30-
def from_mapping(**kwargs: dict[str, Any]) -> LC_Config:
31-
for key in (
32-
"requirements_paths",
33-
"groups",
34-
"extras",
35-
"ignore_packages",
36-
"fail_packages",
37-
"fail_licenses",
38-
"only_licenses",
39-
"skip_dependencies",
40-
"hide_output_parameters",
41-
):
42-
kwargs[key] = set(kwargs.get(key) or ())
43-
return LC_Config(**kwargs)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Any
2+
3+
from pydantic import BaseModel, model_validator
4+
5+
6+
class DefaultOnNoneModel(BaseModel):
7+
@model_validator(mode="before")
8+
@classmethod
9+
def default_on_none(cls, values: Any) -> Any | dict[Any, Any]:
10+
if not isinstance(values, dict):
11+
return values
12+
13+
result = dict(values)
14+
15+
for name, field in cls.model_fields.items():
16+
if name in result and result[name] is None:
17+
if field.default_factory is not None:
18+
result[name] = field.default_factory()
19+
elif field.default is not None:
20+
result[name] = field.default
21+
22+
return result

licensecheck/models/pypijson.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from datetime import UTC, datetime
2+
3+
from pydantic import Field, HttpUrl
4+
5+
from licensecheck.models.defaultonnone import DefaultOnNoneModel
6+
7+
DEFAULT_URL = HttpUrl("http://example.com")
8+
9+
10+
class Downloads(DefaultOnNoneModel):
11+
last_day: int = 0
12+
last_month: int = 0
13+
last_week: int = 0
14+
15+
16+
class Digests(DefaultOnNoneModel):
17+
blake2b_256: str = ""
18+
md5: str = ""
19+
sha256: str = ""
20+
21+
22+
class Info(DefaultOnNoneModel):
23+
author: str = ""
24+
author_email: str = ""
25+
bugtrack_url: str = ""
26+
classifiers: list[str] = Field(default_factory=list)
27+
description: str = ""
28+
description_content_type: str = ""
29+
docs_url: str = ""
30+
download_url: str = ""
31+
downloads: Downloads = Downloads()
32+
dynamic: list[str] = Field(default_factory=list)
33+
home_page: str = ""
34+
keywords: str = ""
35+
license: str = ""
36+
license_expression: str = ""
37+
license_files: list[str] | None = None
38+
maintainer: str = ""
39+
maintainer_email: str = ""
40+
name: str = ""
41+
package_url: HttpUrl = DEFAULT_URL
42+
platform: str = ""
43+
project_url: HttpUrl = DEFAULT_URL
44+
project_urls: dict[str, HttpUrl] = Field(default_factory=dict)
45+
provides_extra: list[str] = Field(default_factory=list)
46+
release_url: HttpUrl = DEFAULT_URL
47+
requires_dist: list[str] | None = None
48+
requires_python: str = ""
49+
summary: str = ""
50+
version: str = ""
51+
yanked: bool = False
52+
yanked_reason: str = ""
53+
54+
55+
class File(DefaultOnNoneModel):
56+
comment_text: str = ""
57+
digests: Digests = Digests()
58+
downloads: int = 0
59+
filename: str = ""
60+
has_sig: bool = False
61+
md5_digest: str = ""
62+
packagetype: str = ""
63+
python_version: str = ""
64+
requires_python: str = ""
65+
size: int | None = None
66+
upload_time: datetime = datetime(1970, 1, 1, tzinfo=UTC)
67+
upload_time_iso_8601: datetime = datetime(1970, 1, 1, tzinfo=UTC)
68+
url: HttpUrl = DEFAULT_URL
69+
yanked: bool = False
70+
yanked_reason: str = ""
71+
72+
73+
class OwnershipRole(DefaultOnNoneModel):
74+
role: str = ""
75+
user: str = ""
76+
77+
78+
class Ownership(DefaultOnNoneModel):
79+
roles: list[OwnershipRole]
80+
organization: str = ""
81+
82+
83+
class Vulnerability(DefaultOnNoneModel):
84+
# PyPI's schema can evolve; keep flexible unless you need validation.
85+
pass
86+
87+
88+
class ProjectResponse(DefaultOnNoneModel):
89+
info: Info = Info()
90+
last_serial: int = 1
91+
urls: list[File] = Field(default_factory=list)
92+
vulnerabilities: list[dict] = Field(default_factory=list)
93+
ownership: Ownership | None = None

0 commit comments

Comments
 (0)