1from __future__ import absolute_import
2from __future__ import unicode_literals
3
4import logging
5
6from typing import Any
7from typing import Dict
8from typing import List
9from typing import Optional
10from typing import Union
11from warnings import warn
12
13from .json import validate_object
14from .packages.dependency import Dependency
15from .packages.project_package import ProjectPackage
16from .poetry import Poetry
17from .pyproject import PyProjectTOML
18from .spdx import license_by_id
19from .utils._compat import Path
20
21
22logger = logging.getLogger(__name__)
23
24
25class Factory(object):
26    """
27    Factory class to create various elements needed by Poetry.
28    """
29
30    def create_poetry(
31        self, cwd=None, with_dev=True
32    ):  # type: (Optional[Path], bool) -> Poetry
33        poetry_file = self.locate(cwd)
34        local_config = PyProjectTOML(path=poetry_file).poetry_config
35
36        # Checking validity
37        check_result = self.validate(local_config)
38        if check_result["errors"]:
39            message = ""
40            for error in check_result["errors"]:
41                message += "  - {}\n".format(error)
42
43            raise RuntimeError("The Poetry configuration is invalid:\n" + message)
44
45        # Load package
46        name = local_config["name"]
47        version = local_config["version"]
48        package = ProjectPackage(name, version, version)
49        package.root_dir = poetry_file.parent
50
51        for author in local_config["authors"]:
52            package.authors.append(author)
53
54        for maintainer in local_config.get("maintainers", []):
55            package.maintainers.append(maintainer)
56
57        package.description = local_config.get("description", "")
58        package.homepage = local_config.get("homepage")
59        package.repository_url = local_config.get("repository")
60        package.documentation_url = local_config.get("documentation")
61        try:
62            license_ = license_by_id(local_config.get("license", ""))
63        except ValueError:
64            license_ = None
65
66        package.license = license_
67        package.keywords = local_config.get("keywords", [])
68        package.classifiers = local_config.get("classifiers", [])
69
70        if "readme" in local_config:
71            package.readme = Path(poetry_file.parent) / local_config["readme"]
72
73        if "platform" in local_config:
74            package.platform = local_config["platform"]
75
76        if "dependencies" in local_config:
77            for name, constraint in local_config["dependencies"].items():
78                if name.lower() == "python":
79                    package.python_versions = constraint
80                    continue
81
82                if isinstance(constraint, list):
83                    for _constraint in constraint:
84                        package.add_dependency(
85                            self.create_dependency(
86                                name, _constraint, root_dir=package.root_dir
87                            )
88                        )
89
90                    continue
91
92                package.add_dependency(
93                    self.create_dependency(name, constraint, root_dir=package.root_dir)
94                )
95
96        if with_dev and "dev-dependencies" in local_config:
97            for name, constraint in local_config["dev-dependencies"].items():
98                if isinstance(constraint, list):
99                    for _constraint in constraint:
100                        package.add_dependency(
101                            self.create_dependency(
102                                name,
103                                _constraint,
104                                category="dev",
105                                root_dir=package.root_dir,
106                            )
107                        )
108
109                    continue
110
111                package.add_dependency(
112                    self.create_dependency(
113                        name, constraint, category="dev", root_dir=package.root_dir
114                    )
115                )
116
117        extras = local_config.get("extras", {})
118        for extra_name, requirements in extras.items():
119            package.extras[extra_name] = []
120
121            # Checking for dependency
122            for req in requirements:
123                req = Dependency(req, "*")
124
125                for dep in package.requires:
126                    if dep.name == req.name:
127                        dep.in_extras.append(extra_name)
128                        package.extras[extra_name].append(dep)
129
130                        break
131
132        if "build" in local_config:
133            build = local_config["build"]
134            if not isinstance(build, dict):
135                build = {"script": build}
136            package.build_config = build or {}
137
138        if "include" in local_config:
139            package.include = []
140
141            for include in local_config["include"]:
142                if not isinstance(include, dict):
143                    include = {"path": include}
144
145                formats = include.get("format", [])
146                if formats and not isinstance(formats, list):
147                    formats = [formats]
148                include["format"] = formats
149
150                package.include.append(include)
151
152        if "exclude" in local_config:
153            package.exclude = local_config["exclude"]
154
155        if "packages" in local_config:
156            package.packages = local_config["packages"]
157
158        # Custom urls
159        if "urls" in local_config:
160            package.custom_urls = local_config["urls"]
161
162        return Poetry(poetry_file, local_config, package)
163
164    @classmethod
165    def create_dependency(
166        cls,
167        name,  # type: str
168        constraint,  # type: Union[str, Dict[str, Any]]
169        category="main",  # type: str
170        root_dir=None,  # type: Optional[Path]
171    ):  # type: (...) -> Dependency
172        from .packages.constraints import parse_constraint as parse_generic_constraint
173        from .packages.directory_dependency import DirectoryDependency
174        from .packages.file_dependency import FileDependency
175        from .packages.url_dependency import URLDependency
176        from .packages.utils.utils import create_nested_marker
177        from .packages.vcs_dependency import VCSDependency
178        from .version.markers import AnyMarker
179        from .version.markers import parse_marker
180
181        if constraint is None:
182            constraint = "*"
183
184        if isinstance(constraint, dict):
185            optional = constraint.get("optional", False)
186            python_versions = constraint.get("python")
187            platform = constraint.get("platform")
188            markers = constraint.get("markers")
189            if "allows-prereleases" in constraint:
190                message = (
191                    'The "{}" dependency specifies '
192                    'the "allows-prereleases" property, which is deprecated. '
193                    'Use "allow-prereleases" instead.'.format(name)
194                )
195                warn(message, DeprecationWarning)
196                logger.warning(message)
197
198            allows_prereleases = constraint.get(
199                "allow-prereleases", constraint.get("allows-prereleases", False)
200            )
201
202            if "git" in constraint:
203                # VCS dependency
204                dependency = VCSDependency(
205                    name,
206                    "git",
207                    constraint["git"],
208                    branch=constraint.get("branch", None),
209                    tag=constraint.get("tag", None),
210                    rev=constraint.get("rev", None),
211                    category=category,
212                    optional=optional,
213                    develop=constraint.get("develop", False),
214                    extras=constraint.get("extras", []),
215                )
216            elif "file" in constraint:
217                file_path = Path(constraint["file"])
218
219                dependency = FileDependency(
220                    name,
221                    file_path,
222                    category=category,
223                    base=root_dir,
224                    extras=constraint.get("extras", []),
225                )
226            elif "path" in constraint:
227                path = Path(constraint["path"])
228
229                if root_dir:
230                    is_file = root_dir.joinpath(path).is_file()
231                else:
232                    is_file = path.is_file()
233
234                if is_file:
235                    dependency = FileDependency(
236                        name,
237                        path,
238                        category=category,
239                        optional=optional,
240                        base=root_dir,
241                        extras=constraint.get("extras", []),
242                    )
243                else:
244                    dependency = DirectoryDependency(
245                        name,
246                        path,
247                        category=category,
248                        optional=optional,
249                        base=root_dir,
250                        develop=constraint.get("develop", False),
251                        extras=constraint.get("extras", []),
252                    )
253            elif "url" in constraint:
254                dependency = URLDependency(
255                    name,
256                    constraint["url"],
257                    category=category,
258                    optional=optional,
259                    extras=constraint.get("extras", []),
260                )
261            else:
262                version = constraint["version"]
263
264                dependency = Dependency(
265                    name,
266                    version,
267                    optional=optional,
268                    category=category,
269                    allows_prereleases=allows_prereleases,
270                    extras=constraint.get("extras", []),
271                )
272
273            if not markers:
274                marker = AnyMarker()
275                if python_versions:
276                    dependency.python_versions = python_versions
277                    marker = marker.intersect(
278                        parse_marker(
279                            create_nested_marker(
280                                "python_version", dependency.python_constraint
281                            )
282                        )
283                    )
284
285                if platform:
286                    marker = marker.intersect(
287                        parse_marker(
288                            create_nested_marker(
289                                "sys_platform", parse_generic_constraint(platform)
290                            )
291                        )
292                    )
293            else:
294                marker = parse_marker(markers)
295
296            if not marker.is_any():
297                dependency.marker = marker
298
299            dependency.source_name = constraint.get("source")
300        else:
301            dependency = Dependency(name, constraint, category=category)
302
303        return dependency
304
305    @classmethod
306    def validate(
307        cls, config, strict=False
308    ):  # type: (dict, bool) -> Dict[str, List[str]]
309        """
310        Checks the validity of a configuration
311        """
312        result = {"errors": [], "warnings": []}
313        # Schema validation errors
314        validation_errors = validate_object(config, "poetry-schema")
315
316        result["errors"] += validation_errors
317
318        if strict:
319            # If strict, check the file more thoroughly
320            if "dependencies" in config:
321                python_versions = config["dependencies"]["python"]
322                if python_versions == "*":
323                    result["warnings"].append(
324                        "A wildcard Python dependency is ambiguous. "
325                        "Consider specifying a more explicit one."
326                    )
327
328                for name, constraint in config["dependencies"].items():
329                    if not isinstance(constraint, dict):
330                        continue
331
332                    if "allows-prereleases" in constraint:
333                        result["warnings"].append(
334                            'The "{}" dependency specifies '
335                            'the "allows-prereleases" property, which is deprecated. '
336                            'Use "allow-prereleases" instead.'.format(name)
337                        )
338
339            # Checking for scripts with extras
340            if "scripts" in config:
341                scripts = config["scripts"]
342                for name, script in scripts.items():
343                    if not isinstance(script, dict):
344                        continue
345
346                    extras = script["extras"]
347                    for extra in extras:
348                        if extra not in config["extras"]:
349                            result["errors"].append(
350                                'Script "{}" requires extra "{}" which is not defined.'.format(
351                                    name, extra
352                                )
353                            )
354
355        return result
356
357    @classmethod
358    def locate(cls, cwd):  # type: (Path) -> Path
359        candidates = [Path(cwd)]
360        candidates.extend(Path(cwd).parents)
361
362        for path in candidates:
363            poetry_file = path / "pyproject.toml"
364
365            if poetry_file.exists():
366                return poetry_file
367
368        else:
369            raise RuntimeError(
370                "Poetry could not find a pyproject.toml file in {} or its parents".format(
371                    cwd
372                )
373            )
374