# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
-import ast
from pathlib import Path
import re
-import shutil
import string
+from typing import Callable, List, Optional, Pattern, Tuple, Set
+from dataclasses import dataclass
+import logging
-from recommonmark.parser import CommonMarkParser
+from pkg_resources import get_distribution
+logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
-CURRENT_DIR = Path(__file__).parent
-
-
-def get_version():
- black_py = CURRENT_DIR / '..' / 'black.py'
- _version_re = re.compile(r'__version__\s+=\s+(?P<version>.*)')
- with open(str(black_py), 'r', encoding='utf8') as f:
- version = _version_re.search(f.read()).group('version')
- return str(ast.literal_eval(version))
-
+LOG = logging.getLogger(__name__)
-def make_pypi_svg(version):
- template = CURRENT_DIR / '_static' / 'pypi_template.svg'
- target = CURRENT_DIR / '_static' / 'pypi.svg'
- with open(str(template), 'r', encoding='utf8') as f:
- svg = string.Template(f.read()).substitute(version=version)
- with open(str(target), 'w', encoding='utf8') as f:
+CURRENT_DIR = Path(__file__).parent
+README = CURRENT_DIR / ".." / "README.md"
+REFERENCE_DIR = CURRENT_DIR / "reference"
+STATIC_DIR = CURRENT_DIR / "_static"
+
+
+@dataclass
+class SrcRange:
+ """Tracks which part of a file to get a section's content.
+
+ Data:
+ start_line: The line where the section starts (i.e. its sub-header) (inclusive).
+ end_line: The line where the section ends (usually next sub-header) (exclusive).
+ """
+
+ start_line: int
+ end_line: int
+
+
+@dataclass
+class DocSection:
+ """Tracks information about a section of documentation.
+
+ Data:
+ name: The section's name. This will used to detect duplicate sections.
+ src: The filepath to get its contents.
+ processors: The processors to run before writing the section to CURRENT_DIR.
+ out_filename: The filename to use when writing the section to CURRENT_DIR.
+ src_range: The line range of SRC to gets its contents.
+ """
+
+ name: str
+ src: Path
+ src_range: SrcRange = SrcRange(0, 1_000_000)
+ out_filename: str = ""
+ processors: Tuple[Callable, ...] = ()
+
+ def get_out_filename(self) -> str:
+ if not self.out_filename:
+ return self.name + ".md"
+ else:
+ return self.out_filename
+
+
+def make_pypi_svg(version: str) -> None:
+ template: Path = CURRENT_DIR / "_static" / "pypi_template.svg"
+ target: Path = CURRENT_DIR / "_static" / "pypi.svg"
+ with open(str(template), "r", encoding="utf8") as f:
+ svg: str = string.Template(f.read()).substitute(version=version)
+ with open(str(target), "w", encoding="utf8") as f:
f.write(svg)
-def make_filename(line):
- non_letters = re.compile(r'[^a-z]+')
- filename = line[3:].rstrip().lower()
- filename = non_letters.sub('_', filename)
- if filename.startswith('_'):
+def make_filename(line: str) -> str:
+ non_letters: Pattern = re.compile(r"[^a-z]+")
+ filename: str = line[3:].rstrip().lower()
+ filename = non_letters.sub("_", filename)
+ if filename.startswith("_"):
filename = filename[1:]
- if filename.endswith('_'):
+ if filename.endswith("_"):
filename = filename[:-1]
- return filename + '.md'
-
-
-def generate_sections_from_readme():
- target_dir = CURRENT_DIR / '_build' / 'generated'
- readme = CURRENT_DIR / '..' / 'README.md'
- shutil.rmtree(str(target_dir), ignore_errors=True)
- target_dir.mkdir(parents=True)
-
- output = None
- target_dir = target_dir.relative_to(CURRENT_DIR)
- with open(str(readme), 'r', encoding='utf8') as f:
- for line in f:
- if line.startswith('## '):
- if output is not None:
- output.close()
+ return filename + ".md"
+
+
+def get_contents(section: DocSection) -> str:
+ """Gets the contents for the DocSection."""
+ contents: List[str] = []
+ src: Path = section.src
+ start_line: int = section.src_range.start_line
+ end_line: int = section.src_range.end_line
+ with open(src, "r", encoding="utf-8") as f:
+ for lineno, line in enumerate(f, start=1):
+ if lineno >= start_line and lineno < end_line:
+ contents.append(line)
+ return "".join(contents)
+
+
+def get_sections_from_readme() -> List[DocSection]:
+ """Gets the sections from README so they can be processed by process_sections.
+
+ It opens README and goes down line by line looking for sub-header lines which
+ denotes a section. Once it finds a sub-header line, it will create a DocSection
+ object with all of the information currently available. Then on every line, it will
+ track the ending line index of the section. And it repeats this for every sub-header
+ line it finds.
+ """
+ sections: List[DocSection] = []
+ section: Optional[DocSection] = None
+ with open(README, "r", encoding="utf-8") as f:
+ for lineno, line in enumerate(f, start=1):
+ if line.startswith("## "):
filename = make_filename(line)
- output_path = CURRENT_DIR / filename
- if output_path.is_symlink() or output_path.is_file():
- output_path.unlink()
- output_path.symlink_to(target_dir / filename)
- output = open(str(output_path), 'w', encoding='utf8')
- output.write(
- '[//]: # (NOTE: THIS FILE IS AUTOGENERATED FROM README.md)\n\n'
+ section_name = filename[:-3]
+ section = DocSection(
+ name=str(section_name),
+ src=README,
+ src_range=SrcRange(lineno, lineno),
+ out_filename=filename,
+ processors=(fix_headers,),
)
-
- if output is None:
- continue
-
- if line.startswith('##'):
- line = line[1:]
-
- output.write(line)
+ sections.append(section)
+ if section is not None:
+ section.src_range.end_line += 1
+ return sections
+
+
+def fix_headers(contents: str) -> str:
+ """Fixes the headers of sections copied from README.
+
+ Removes one octothorpe (#) from all headers since the contents are no longer nested
+ in a root document (i.e. the README).
+ """
+ lines: List[str] = contents.splitlines()
+ fixed_contents: List[str] = []
+ for line in lines:
+ if line.startswith("##"):
+ line = line[1:]
+ fixed_contents.append(line + "\n") # splitlines strips the leading newlines
+ return "".join(fixed_contents)
+
+
+def process_sections(
+ custom_sections: List[DocSection], readme_sections: List[DocSection]
+) -> None:
+ """Reads, processes, and writes sections to CURRENT_DIR.
+
+ For each section, the contents will be fetched, processed by processors
+ required by the section, and written to CURRENT_DIR. If it encounters duplicate
+ sections (i.e. shares the same name attribute), it will skip processing the
+ duplicates.
+
+ It processes custom sections before the README generated sections so sections in the
+ README can be overwritten with custom options.
+ """
+ processed_sections: Set[str] = set()
+ modified_files: Set[Path] = set()
+ sections: List[DocSection] = custom_sections
+ sections.extend(readme_sections)
+ for section in sections:
+ LOG.info(f"Processing '{section.name}' from {section.src}")
+ if section.name in processed_sections:
+ LOG.info(
+ f"Skipping '{section.name}' from '{section.src}' as it is a duplicate"
+ )
+ continue
+
+ target_path: Path = CURRENT_DIR / section.get_out_filename()
+ if target_path in modified_files:
+ LOG.warning(
+ f"{target_path} has been already written to, its contents will be"
+ " OVERWRITTEN and notices will be duplicated"
+ )
+ contents: str = get_contents(section)
+
+ # processors goes here
+ if fix_headers in section.processors:
+ contents = fix_headers(contents)
+
+ with open(target_path, "w", encoding="utf-8") as f:
+ if section.src.suffix == ".md" and section.src != target_path:
+ rel = section.src.resolve().relative_to(CURRENT_DIR.parent)
+ f.write(f'[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM {rel}"\n\n')
+ f.write(contents)
+ processed_sections.add(section.name)
+ modified_files.add(target_path)
# -- Project information -----------------------------------------------------
-project = 'Black'
-copyright = '2018, Łukasz Langa and contributors to Black'
-author = 'Łukasz Langa and contributors to Black'
+project = "Black"
+copyright = "2020, Łukasz Langa and contributors to Black"
+author = "Łukasz Langa and contributors to Black"
# Autopopulate version
-# The full version, including alpha/beta/rc tags.
-release = get_version()
+# The version, including alpha/beta/rc tags, but not commit hash and datestamps
+release = get_distribution("black").version.split("+")[0]
# The short X.Y version.
version = release
-for sp in 'abcfr':
+for sp in "abcfr":
version = version.split(sp)[0]
+
+custom_sections = [
+ DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md"),
+ DocSection("editor_integration", CURRENT_DIR / "editor_integration.md"),
+ DocSection("blackd", CURRENT_DIR / "blackd.md"),
+ DocSection("black_primer", CURRENT_DIR / "black_primer.md"),
+ DocSection("contributing_to_black", CURRENT_DIR / ".." / "CONTRIBUTING.md"),
+ DocSection("change_log", CURRENT_DIR / ".." / "CHANGES.md"),
+]
+
+# Sphinx complains when there is a source file that isn't referenced in any of the docs.
+# Since some sections autogenerated from the README are unused warnings will appear.
+#
+# Sections must be listed to what their name is when passed through make_filename().
+blocklisted_sections_from_readme = {
+ "license",
+ "pragmatism",
+ "testimonials",
+ "used_by",
+}
+
make_pypi_svg(release)
-generate_sections_from_readme()
+readme_sections = get_sections_from_readme()
+readme_sections = [
+ x for x in readme_sections if x.name not in blocklisted_sections_from_readme
+]
+
+process_sections(custom_sections, readme_sections)
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#
-# needs_sphinx = '1.0'
+needs_sphinx = "3.0"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.intersphinx',
- 'sphinx.ext.napoleon',
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.napoleon",
+ "recommonmark",
]
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+# If you need extensions of a certain version or higher, list them here.
+needs_extensions = {"recommonmark": "0.5"}
-source_parsers = {
- '.md': CommonMarkParser,
-}
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
-source_suffix = ['.rst', '.md']
+source_suffix = [".rst", ".md"]
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'alabaster'
+html_theme = "alabaster"
html_sidebars = {
- '**': [
- 'about.html',
- 'navigation.html',
- 'relations.html',
- 'sourcelink.html',
- 'searchbox.html'
+ "**": [
+ "about.html",
+ "navigation.html",
+ "relations.html",
+ "sourcelink.html",
+ "searchbox.html",
]
}
html_theme_options = {
- 'show_related': False,
- 'description': '“Any color you like.”',
- 'github_button': True,
- 'github_user': 'ambv',
- 'github_repo': 'black',
- 'github_type': 'star',
- 'show_powered_by': True,
- 'fixed_sidebar': True,
- 'logo': 'logo2.png',
+ "show_related": False,
+ "description": "“Any color you like.”",
+ "github_button": True,
+ "github_user": "psf",
+ "github_repo": "black",
+ "github_type": "star",
+ "show_powered_by": True,
+ "fixed_sidebar": True,
+ "logo": "logo2.png",
+ "travis_button": True,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
-htmlhelp_basename = 'blackdoc'
+htmlhelp_basename = "blackdoc"
# -- Options for LaTeX output ------------------------------------------------
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
-
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
-
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
-
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'black.tex', 'Documentation for Black',
- 'Łukasz Langa and contributors to Black', 'manual'),
+ (
+ master_doc,
+ "black.tex",
+ "Documentation for Black",
+ "Łukasz Langa and contributors to Black",
+ "manual",
+ )
]
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'black', 'Documentation for Black',
- [author], 1)
-]
+man_pages = [(master_doc, "black", "Documentation for Black", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'Black', 'Documentation for Black',
- author, 'Black', 'The uncompromising Python code formatter',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "Black",
+ "Documentation for Black",
+ author,
+ "Black",
+ "The uncompromising Python code formatter",
+ "Miscellaneous",
+ )
]
# epub_uid = ''
# A list of files that should not be packed into the epub file.
-epub_exclude_files = ['search.html']
+epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
-autodoc_member_order = 'bysource'
+autodoc_member_order = "bysource"
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/3/': None}
+intersphinx_mapping = {"https://docs.python.org/3/": None}