|
27 | 27 | VERSION_REGEX = r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]'
|
28 | 28 | NEW_REQ_PACKAGES = ["azure-core", "azure-mgmt-core"]
|
29 | 29 |
|
| 30 | +INIT_PY_FILE = "__init__.py" |
| 31 | +INIT_EXTENSION_SUBSTRING = ".extend_path(__path__, __name__)" |
| 32 | + |
| 33 | +# Directories to exclude from searches to avoid finding files in wrong places |
| 34 | +EXCLUDE = { |
| 35 | + "venv", |
| 36 | + "__pycache__", |
| 37 | + "tests", |
| 38 | + "test", |
| 39 | + "generated_samples", |
| 40 | + "generated_tests", |
| 41 | + "samples", |
| 42 | + "swagger", |
| 43 | + "stress", |
| 44 | + "docs", |
| 45 | + "doc", |
| 46 | + "local", |
| 47 | + "scripts", |
| 48 | + "images", |
| 49 | + ".tox" |
| 50 | +} |
| 51 | + |
| 52 | + |
| 53 | +def discover_namespace(package_root_path: str) -> Optional[str]: |
| 54 | + """ |
| 55 | + Discover the true namespace of a package by walking through its directory structure |
| 56 | + and finding the first __init__.py that contains actual content (not just namespace extension). |
| 57 | + |
| 58 | + :param str package_root_path: Root path of the package directory |
| 59 | + :rtype: str or None |
| 60 | + :return: The discovered namespace string, or None if no suitable namespace found |
| 61 | + """ |
| 62 | + if not os.path.exists(package_root_path): |
| 63 | + return None |
| 64 | + |
| 65 | + namespace = None |
| 66 | + |
| 67 | + for root, subdirs, files in os.walk(package_root_path): |
| 68 | + # Ignore any modules with name starts with "_" |
| 69 | + # For e.g. _generated, _shared etc |
| 70 | + # Ignore build, which is created when installing a package from source. |
| 71 | + # Ignore tests, which may have an __init__.py but is not part of the package. |
| 72 | + dirs_to_skip = [x for x in subdirs if x.startswith(("_", ".", "test", "build")) or x in EXCLUDE] |
| 73 | + for d in dirs_to_skip: |
| 74 | + logging.debug("Dirs to skip: {}".format(dirs_to_skip)) |
| 75 | + subdirs.remove(d) |
| 76 | + |
| 77 | + if INIT_PY_FILE in files: |
| 78 | + module_name = os.path.relpath(root, package_root_path).replace( |
| 79 | + os.path.sep, "." |
| 80 | + ) |
| 81 | + |
| 82 | + # If namespace has not been set yet, try to find the first __init__.py that's not purely for extension. |
| 83 | + if not namespace: |
| 84 | + namespace = _set_root_namespace( |
| 85 | + os.path.join(root, INIT_PY_FILE), module_name |
| 86 | + ) |
| 87 | + |
| 88 | + return namespace |
| 89 | + |
| 90 | + |
| 91 | +def _set_root_namespace(init_file_path: str, module_name: str) -> Optional[str]: |
| 92 | + """ |
| 93 | + Examine an __init__.py file to determine if it represents a substantial namespace |
| 94 | + or is just a namespace extension file. |
| 95 | + |
| 96 | + :param str init_file_path: Path to the __init__.py file |
| 97 | + :param str module_name: The module name corresponding to this __init__.py |
| 98 | + :rtype: str or None |
| 99 | + :return: The namespace if this file contains substantial content, None otherwise |
| 100 | + """ |
| 101 | + try: |
| 102 | + with open(init_file_path, "r", encoding="utf-8") as f: |
| 103 | + in_docstring = False |
| 104 | + content = [] |
| 105 | + for line in f: |
| 106 | + stripped_line = line.strip() |
| 107 | + # If in multi-line docstring, skip following lines until end of docstring. |
| 108 | + # If single-line docstring, skip the docstring line. |
| 109 | + if stripped_line.startswith(('"""', "'''")) and not stripped_line.endswith(('"""', "'''")): |
| 110 | + in_docstring = not in_docstring |
| 111 | + # If comment, skip line. Otherwise, add to content. |
| 112 | + if not in_docstring and not stripped_line.startswith("#"): |
| 113 | + content.append(line) |
| 114 | + |
| 115 | + # If there's more than one line of content, or if there's one line that's not just namespace extension |
| 116 | + if len(content) > 1 or ( |
| 117 | + len(content) == 1 and INIT_EXTENSION_SUBSTRING not in content[0] |
| 118 | + ): |
| 119 | + return module_name |
| 120 | + |
| 121 | + except Exception as e: |
| 122 | + logging.error(f"Error reading {init_file_path}: {e}") |
| 123 | + |
| 124 | + return None |
| 125 | + |
30 | 126 |
|
31 | 127 | class ParsedSetup:
|
32 | 128 | """
|
@@ -394,7 +490,10 @@ def parse_pyproject(
|
394 | 490 | requires = project_config.get("dependencies")
|
395 | 491 | is_new_sdk = name in NEW_REQ_PACKAGES or any(map(lambda x: (parse_require(x).name in NEW_REQ_PACKAGES), requires))
|
396 | 492 |
|
397 |
| - name_space = name.replace("-", ".") |
| 493 | + # Discover the actual namespace by walking the package directory |
| 494 | + package_directory = os.path.dirname(pyproject_filename) |
| 495 | + discovered_namespace = discover_namespace(package_directory) |
| 496 | + name_space = discovered_namespace if discovered_namespace else name.replace("-", ".") |
398 | 497 | package_data = get_value_from_dict(toml_dict, "tool.setuptools.package-data", None)
|
399 | 498 | include_package_data = get_value_from_dict(toml_dict, "tool.setuptools.include-package-data", True)
|
400 | 499 | classifiers = project_config.get("classifiers", [])
|
@@ -430,27 +529,6 @@ def get_version_py(setup_path: str) -> Optional[str]:
|
430 | 529 | """
|
431 | 530 | Given the path to pyproject.toml or setup.py, attempts to find a (_)version.py file and return its location.
|
432 | 531 | """
|
433 |
| - # this list of directories will be excluded from the search for _version.py |
434 |
| - # this is to avoid finding _version.py in the wrong place, such as in tests |
435 |
| - # or in the venv directory or ANYWHERE ELSE that may mess with the parsing. |
436 |
| - EXCLUDE = { |
437 |
| - "venv", |
438 |
| - "__pycache__", |
439 |
| - "tests", |
440 |
| - "test", |
441 |
| - "generated_samples", |
442 |
| - "generated_tests", |
443 |
| - "samples", |
444 |
| - "swagger", |
445 |
| - "stress", |
446 |
| - "docs", |
447 |
| - "doc", |
448 |
| - "local", |
449 |
| - "scripts", |
450 |
| - "images", |
451 |
| - ".tox" |
452 |
| - } |
453 |
| - |
454 | 532 | file_path, _ = os.path.split(setup_path)
|
455 | 533 |
|
456 | 534 | # Find path to _version.py recursively
|
|
0 commit comments