Skip to content

Commit a5381d0

Browse files
authored
Fix azpysdk Sphinx Check and Use in CI (#44144)
* minor bug fix * unzip * cut * fix mgmt apidoc * use constants for repeated strings * move should_build_docs * docstrings * update yml
1 parent 2736523 commit a5381d0

File tree

3 files changed

+115
-48
lines changed

3 files changed

+115
-48
lines changed

eng/pipelines/templates/steps/build-extended-artifacts.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,17 @@ steps:
6767
displayName: 'Generate Docs'
6868
condition: and(succeededOrFailed(), ${{parameters.BuildDocs}})
6969
inputs:
70-
scriptPath: 'scripts/devops_tasks/dispatch_tox.py'
70+
scriptPath: 'eng/scripts/dispatch_checks.py'
7171
arguments: >-
7272
"$(TargetingString)"
7373
--service="${{ parameters.ServiceDirectory }}"
74-
--toxenv=sphinx
74+
--checks="sphinx"
7575
--wheel_dir="$(Build.ArtifactStagingDirectory)"
76-
76+
env:
77+
TOX_PIP_IMPL: "uv"
78+
VIRTUAL_ENV: ""
79+
PYTHONHOME: ""
80+
7781
- ${{ if eq(parameters.RunApiStubGen, 'true') }}:
7882
- template: /eng/pipelines/templates/steps/run_apistub.yml
7983
parameters:

eng/tools/azure-sdk-tools/azpysdk/sphinx.py

Lines changed: 101 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import sys
44
import shutil
55
import glob
6+
import zipfile
7+
import tarfile
68

79
from typing import Optional, List
810
from subprocess import CalledProcessError, check_call
911
from pathlib import Path
1012

1113
from .Check import Check
12-
from ci_tools.functions import install_into_venv
14+
from ci_tools.functions import install_into_venv, unzip_file_to_directory
1315
from ci_tools.scenario.generation import create_package_and_install
1416
from ci_tools.variables import in_ci, set_envvar_defaults
1517
from ci_tools.variables import discover_repo_root
@@ -46,10 +48,55 @@
4648
ci_doc_dir = os.path.join(REPO_ROOT, "_docs")
4749
sphinx_conf_dir = os.path.join(REPO_ROOT, "doc/sphinx")
4850
generate_mgmt_script = os.path.join(REPO_ROOT, "doc/sphinx/generate_doc.py")
51+
UNZIPPPED_DIR_NAME = "unzipped"
52+
DOCGEN_DIR_NAME = "docgen"
53+
54+
55+
def unzip_sdist_to_directory(containing_folder: str) -> str:
56+
"""
57+
Unzips the first .zip or .tar.gz file found in the containing_folder to that same folder.
58+
59+
:param containing_folder: The folder to search for .zip or .tar.gz files.
60+
:return: The path to the directory where the archive was extracted.
61+
"""
62+
zips = glob.glob(os.path.join(containing_folder, "*.zip"))
63+
64+
if zips:
65+
return unzip_file_to_directory(zips[0], containing_folder)
66+
else:
67+
tars = glob.glob(os.path.join(containing_folder, "*.tar.gz"))
68+
69+
if tars:
70+
return unzip_file_to_directory(tars[0], containing_folder)
71+
else:
72+
raise FileNotFoundError("No .zip or .tar.gz files found in {}".format(containing_folder))
73+
74+
75+
def move_and_rename(source_location: str) -> str:
76+
"""
77+
Moves and renames the extracted sdist folder to a known name.
78+
79+
:param source_location: The path to the extracted sdist folder.
80+
:return: The new path after moving and renaming.
81+
"""
82+
new_location = os.path.join(os.path.dirname(source_location), UNZIPPPED_DIR_NAME)
83+
84+
if os.path.exists(new_location):
85+
shutil.rmtree(new_location)
86+
87+
os.rename(source_location, new_location)
88+
return new_location
4989

5090

5191
# env prep helper functions
5292
def create_index_file(readme_location: str, package_rst: str) -> str:
93+
"""
94+
Create the index file content by combining the readme and package rst reference.
95+
96+
:param readme_location: The path to the README file.
97+
:param package_rst: The package rst reference.
98+
:return: The combined index file content.
99+
"""
53100
readme_ext = os.path.splitext(readme_location)[1]
54101

55102
output = ""
@@ -65,6 +112,13 @@ def create_index_file(readme_location: str, package_rst: str) -> str:
65112

66113

67114
def create_index(doc_folder: str, source_location: str, namespace: str) -> None:
115+
"""
116+
Create the index.md file in the documentation folder.
117+
118+
:param doc_folder: The path to the documentation folder.
119+
:param source_location: The path to the source location.
120+
:param namespace: The namespace for the documentation.
121+
"""
68122
index_content = ""
69123

70124
package_rst = "{}.rst".format(namespace)
@@ -174,6 +228,10 @@ def run(self, args: argparse.Namespace) -> int:
174228
logger.error("This tool requires Python 3.11 or newer. Please upgrade your Python interpreter.")
175229
return 1
176230

231+
if not should_build_docs(package_name):
232+
logger.info("Skipping sphinx for {}".format(package_name))
233+
continue
234+
177235
self.install_dev_reqs(executable, args, package_dir)
178236

179237
create_package_and_install(
@@ -220,44 +278,36 @@ def run(self, args: argparse.Namespace) -> int:
220278
logger.info(f"Running sphinx against {package_name}")
221279

222280
# prep env for sphinx
223-
doc_folder = os.path.join(staging_directory, "docgen")
224281
site_folder = os.path.join(package_dir, "website")
282+
doc_folder = os.path.join(staging_directory, DOCGEN_DIR_NAME)
225283

226-
if should_build_docs(package_name):
227-
create_index(doc_folder, package_dir, parsed.namespace)
228-
229-
write_version(site_folder, parsed.version)
230-
else:
231-
logger.info("Skipping sphinx prep for {}".format(package_name))
284+
source_location = move_and_rename(unzip_sdist_to_directory(staging_directory))
285+
doc_folder = os.path.join(source_location, DOCGEN_DIR_NAME)
286+
create_index(doc_folder, source_location, parsed.namespace)
287+
write_version(site_folder, parsed.version)
232288

233289
# run apidoc
234-
if should_build_docs(parsed.name):
235-
if is_mgmt_package(parsed.name):
236-
results.append(self.mgmt_apidoc(doc_folder, package_dir, executable))
237-
else:
238-
results.append(self.sphinx_apidoc(staging_directory, package_dir, parsed.namespace, executable))
290+
output_dir = os.path.join(staging_directory, f"{UNZIPPPED_DIR_NAME}/{DOCGEN_DIR_NAME}")
291+
if is_mgmt_package(package_name):
292+
results.append(self.mgmt_apidoc(output_dir, package_dir, executable))
239293
else:
240-
logger.info("Skipping sphinx source generation for {}".format(parsed.name))
294+
results.append(self.sphinx_apidoc(staging_directory, parsed.namespace, executable))
241295

242296
# build
243-
if should_build_docs(package_name):
244-
# Only data-plane libraries run strict sphinx at the moment
245-
fail_on_warning = not is_mgmt_package(package_name)
246-
results.append(
247-
# doc_folder = source
248-
# site_folder = output
249-
self.sphinx_build(package_dir, doc_folder, site_folder, fail_on_warning, executable)
250-
)
251-
252-
if in_ci() or args.in_ci:
253-
move_output_and_compress(site_folder, package_dir, package_name)
254-
if in_analyze_weekly():
255-
from gh_tools.vnext_issue_creator import close_vnext_issue
297+
# Only data-plane libraries run strict sphinx at the moment
298+
fail_on_warning = not is_mgmt_package(package_name)
299+
results.append(
300+
# doc_folder = source
301+
# site_folder = output
302+
self.sphinx_build(package_dir, doc_folder, site_folder, fail_on_warning, executable)
303+
)
256304

257-
close_vnext_issue(package_name, "sphinx")
305+
if in_ci() or args.in_ci:
306+
move_output_and_compress(site_folder, package_dir, package_name)
307+
if in_analyze_weekly():
308+
from gh_tools.vnext_issue_creator import close_vnext_issue
258309

259-
else:
260-
logger.info("Skipping sphinx build for {}".format(package_name))
310+
close_vnext_issue(package_name, "sphinx")
261311

262312
return max(results) if results else 0
263313

@@ -282,7 +332,9 @@ def sphinx_build(
282332
try:
283333
logger.info("Sphinx build command: {}".format(command_array))
284334

285-
self.run_venv_command(executable, command_array, cwd=package_dir, check=True, append_executable=False)
335+
self.run_venv_command(
336+
executable, command_array, cwd=package_dir, check=True, append_executable=False, immediately_dump=True
337+
)
286338
except CalledProcessError as e:
287339
logger.error("sphinx-build failed for path {} exited with error {}".format(target_dir, e.returncode))
288340
if in_analyze_weekly():
@@ -294,7 +346,6 @@ def sphinx_build(
294346

295347
def mgmt_apidoc(self, output_dir: str, target_folder: str, executable: str) -> int:
296348
command_array = [
297-
executable,
298349
generate_mgmt_script,
299350
"-p",
300351
target_folder,
@@ -306,48 +357,53 @@ def mgmt_apidoc(self, output_dir: str, target_folder: str, executable: str) -> i
306357
try:
307358
logger.info("Command to generate management sphinx sources: {}".format(command_array))
308359

309-
self.run_venv_command(executable, command_array, cwd=target_folder, check=True, append_executable=False)
360+
self.run_venv_command(
361+
executable, command_array, cwd=target_folder, check=True, append_executable=True, immediately_dump=True
362+
)
310363
except CalledProcessError as e:
311364
logger.error("script failed for path {} exited with error {}".format(output_dir, e.returncode))
312365
return 1
313366
return 0
314367

315-
def sphinx_apidoc(self, output_dir: str, target_dir: str, namespace: str, executable: str) -> int:
316-
working_doc_folder = os.path.join(output_dir, "doc")
368+
def sphinx_apidoc(self, target_dir: str, namespace: str, executable: str) -> int:
369+
working_doc_folder = os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/doc")
317370
command_array = [
318371
"sphinx-apidoc",
319372
"--no-toc",
320373
"--module-first",
321374
"-o",
322-
os.path.join(output_dir, "docgen"), # This is the output folder
323-
os.path.join(target_dir, ""), # This is the input folder
324-
os.path.join(target_dir, "test*"), # This argument and below are "exclude" directory arguments
325-
os.path.join(target_dir, "example*"),
326-
os.path.join(target_dir, "sample*"),
327-
os.path.join(target_dir, "setup.py"),
328-
os.path.join(target_dir, "conftest.py"),
375+
os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/{DOCGEN_DIR_NAME}"), # This is the output folder
376+
os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/"), # This is the input folder
377+
os.path.join(
378+
target_dir, f"{UNZIPPPED_DIR_NAME}/test*"
379+
), # This argument and below are "exclude" directory arguments
380+
os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/example*"),
381+
os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/sample*"),
382+
os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/setup.py"),
329383
]
330384

331385
try:
332386
# if a `doc` folder exists, just leverage the sphinx sources found therein.
333387
if os.path.exists(working_doc_folder):
334388
logger.info("Copying files into sphinx source folder.")
335-
copy_existing_docs(working_doc_folder, os.path.join(output_dir, "docgen"))
389+
copy_existing_docs(
390+
working_doc_folder, os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/{DOCGEN_DIR_NAME}")
391+
)
336392

337393
# otherwise, we will run sphinx-apidoc to generate the sources
338394
else:
339395
logger.info("Sphinx api-doc command: {}".format(command_array))
340396
self.run_venv_command(executable, command_array, cwd=target_dir, check=True, append_executable=False)
341397
# We need to clean "azure.rst", and other RST before the main namespaces, as they are never
342398
# used and will log as a warning later by sphinx-build, which is blocking strict_sphinx
343-
base_path = Path(os.path.join(output_dir, "docgen/"))
399+
base_path = Path(os.path.join(target_dir, f"{UNZIPPPED_DIR_NAME}/{DOCGEN_DIR_NAME}/"))
344400
namespace = namespace.rpartition(".")[0]
345401
while namespace:
346402
rst_file_to_delete = base_path / f"{namespace}.rst"
347403
logger.info(f"Removing {rst_file_to_delete}")
348404
rst_file_to_delete.unlink(missing_ok=True)
349405
namespace = namespace.rpartition(".")[0]
350406
except CalledProcessError as e:
351-
logger.error("sphinx-apidoc failed for path {} exited with error {}".format(output_dir, e.returncode))
407+
logger.error("sphinx-apidoc failed for path {} exited with error {}".format(target_dir, e.returncode))
352408
return 1
353409
return 0

eng/tools/azure-sdk-tools/ci_tools/functions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@
9292

9393

9494
def unzip_file_to_directory(path_to_zip_file: str, extract_location: str) -> str:
95+
"""
96+
Unzips a zip or tar.gz file to a given location.
97+
98+
:param path_to_zip_file: The path to the zip or tar.gz file.
99+
:param extract_location: The directory where the contents will be extracted.
100+
:return: The path to the directory where the archive was extracted.
101+
"""
95102
if path_to_zip_file.endswith(".zip"):
96103
with zipfile.ZipFile(path_to_zip_file, "r") as zip_ref:
97104
zip_ref.extractall(extract_location)

0 commit comments

Comments
 (0)