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