Source code for make_dist

#!/usr/bin/env python3
#
# (C) Pywikibot team, 2022-2026
#
# Distributed under the terms of the MIT license.
#
"""Script to create a new distribution.

The following options are supported:

pywikibot  The pywikibot repository to build (default)

scripts    The pywikibot-scripts repository to build

-help      Print documentation of this file and of setup.py

-local     Install the distribution as a local site-package. If a
           Pywikibot package is already there, it will be uninstalled
           first. Clears old dist folders first.

-remote    Upload the package to pypi. This cannot be done if the
           Pywikibot version is a development release.  Clears old dist
           folders first.

-clear     Clear old dist folders and leave. Does not create a
           distribution.

-upgrade   Upgrade pip first; upgrade or install distribution packages
           build and twine first.

Usage::

    python -B -m [pwb] make_dist [repo] [options]

.. version-added:: 7.3
.. version-changed:: 7.4

   - updates pip, setuptools, wheel and twine packages first
   - installs pre-releases over stable versions
   - also creates built distribution together with source distribution
   - *-upgrade* option was added

.. version-changed:: 7.5

   - *clear* option was added
   - *nodist* option was added

.. version-changed:: 8.1
   *nodist* option was removed, *clear* option does not create a
   distribution. *local* and *remote* option clears old distributions
   first.
.. version-changed:: 8.2
   Build frontend was changed from setuptools to build. ``-upgrade``
   option also installs packages if necessary.

.. version-changed:: 9.4
   The pywikibot-scripts distribution can be created.
"""
from __future__ import annotations

import abc
import os
import shutil
import sys
from contextlib import suppress
from dataclasses import dataclass, field
from importlib.util import find_spec
from pathlib import Path
from subprocess import check_call, run

from pywikibot import __version__, error, info, input_yn, warning


MODULE, COMMAND = range(2)
pip = f'{sys.executable} -m pip'


[docs] @dataclass class SetupBase(abc.ABC): """Setup distribution base class. .. version-added:: 8.0 .. version-changed:: 8.1 *dataclass* is used. .. version-changed:: 11.1 Use ``sys.executable`` to determine the Python Python interpreter executing this script. """ local: bool remote: bool clear: bool upgrade: bool build_opt: str = field(init=False) folder: Path = field(init=False) def __post_init__(self) -> None: """Post-init initializer.""" self.folder = Path.cwd()
[docs] def clear_old_dist(self) -> None: # pragma: no cover """Delete old dist folders. .. version-added:: 7.5 .. version-changed:: 11.5 Use pyclean for cleanup. """ info('<<lightyellow>>Removing old dist folders... ', newline=False) check_call('pyclean . -v --debris') info('<<lightyellow>>done')
[docs] @abc.abstractmethod def copy_files(self) -> None: """Copy files."""
[docs] @abc.abstractmethod def cleanup(self) -> None: """Cleanup copied files."""
@staticmethod def _check_module(module: str, module_type: int) -> bool: """Return whether a module or CLI command is available. .. version-added: 11.5 :param module: Module or command name. :param module_type: Type of module, either MODULE or COMMAND. :return: Whether the module or command is available. :raises ValueError: Invalid module type. """ if module_type == MODULE: return find_spec(module) is not None if module_type == COMMAND: return shutil.which(module) is not None raise ValueError(f'Invalid module type: {module_type}')
[docs] def run(self) -> bool: """Run the installer script. :return: True if no error occurs, else False """ tools = ( ('build', MODULE), ('pyclean', COMMAND), ('twine', MODULE), ) for tool, tool_type in tools: if not self._check_module(tool, tool_type): if not self.upgrade: error(f'<<lightred>>{tool} not found') info('<<lightblue>>You may use -upgrade option to install') return False info(f'<<lightyellow>>Install or upgrade {tool}') check_call(f'{pip} install {tool}', shell=True) elif self.upgrade: check_call(f'{pip} install --upgrade {tool}', shell=True) if self.local or self.remote or self.clear: self.clear_old_dist() if self.clear: return True return self.build() # pragma: no cover
[docs] def build(self) -> bool: # pragma: no cover """Build the packages. .. version-added:: 9.3 .. version-changed:: 11.1 Use pure-Python implementation of tokenizer for mwparserfromhell with Windows. """ self.copy_files() info('<<lightyellow>>Build package') try: check_call(f'python -m build {self.build_opt}') except Exception as e: error(e) return False finally: self.cleanup() info('<<lightyellow>>Check package and description') if run('twine check dist/*', shell=True).returncode: return False if self.local: info('<<lightyellow>>Install locally') check_call(f'{pip} uninstall {self.package} -y', shell=True) env = os.environ.copy() if sys.platform.startswith('win32'): # set the WITH_EXTENSION ennvironment variable for mwpfh; # refer the mwpfh documentation env['WITH_EXTENSION'] = '0' check_call(f'{pip} install --no-cache-dir --pre ' f'--find-links=dist {self.package}', shell=True, env=env) if self.remote and input_yn( '<<lightblue>>Upload dist to pypi', automatic_quit=False): check_call('twine upload dist/*', shell=True) return True
[docs] class SetupPywikibot(SetupBase): """Setup for Pywikibot distribution. .. version-added:: 8.0 """ build_opt = '' # defaults to current directory package = 'pywikibot' def __init__(self, *args) -> None: """Set source and target directories.""" super().__init__(*args) source = self.folder / 'scripts' / 'i18n' / 'pywikibot' target = self.folder / 'pywikibot' / 'scripts' / 'i18n' / 'pywikibot' self.target = target self.source = source
[docs] def copy_files(self) -> None: # pragma: no cover """Copy i18n files to pywikibot.scripts folder. Pywikibot i18n files are used for some translations. They are copied to the pywikibot scripts folder. """ info('<<lightyellow>>Copy files') info(f'directory is {self.folder}') info(f'clear {self.target} directory') shutil.rmtree(self.target, ignore_errors=True) info('copy i18n files ... ', newline=False) shutil.copytree(self.source, self.target) info('done')
[docs] def cleanup(self) -> None: # pragma: no cover """Remove all copied files from pywikibot scripts folder.""" info('<<lightyellow>>Remove copied files... ', newline=False) shutil.rmtree(self.target) # restore pywikibot en.json file filename = 'en.json' self.target.mkdir() shutil.copy(self.source / filename, self.target / filename) info('<<lightyellow>>done')
[docs] class SetupScripts(SetupBase): """Setup pywikibot-scripts distribution. .. version-added:: 9.4 """ build_opt = '-w' # only wheel (yet) package = 'pywikibot_scripts' replace = 'MANIFEST.in', 'pyproject.toml', 'setup.py'
[docs] def copy_files(self) -> None: # pragma: no cover """Ignore copy files yet.""" info('<<lightyellow>>Copy files ...', newline=False) for filename in self.replace: file = self.folder / filename file.rename(self.folder / (filename + '.saved')) with suppress(FileNotFoundError): shutil.copy(self.folder / 'scripts' / filename, self.folder) info('<<lightyellow>>done')
[docs] def cleanup(self) -> None: # pragma: no cover """Ignore cleanup yet.""" info('<<lightyellow>>Copy files ...', newline=False) for filename in self.replace: file = self.folder / (filename + '.saved') file.replace(self.folder / filename) info('<<lightyellow>>done')
[docs] def handle_args() -> tuple[bool, bool, bool, bool, bool]: """Handle arguments and print documentation if requested. :return: Return whether dist is to be installed locally or to be uploaded """ if '-help' in sys.argv: # pragma: no cover import re import setup help_text = re.sub(r'^\.\. version(added|changed)::.+', '', __doc__, flags=re.MULTILINE | re.DOTALL) info(help_text) info(setup.__doc__) sys.exit() local = '-local' in sys.argv remote = '-remote' in sys.argv clear = '-clear' in sys.argv upgrade = '-upgrade' in sys.argv scripts = 'scripts' in sys.argv msg = '' if not scripts and remote and 'dev' in __version__: # pragma: no cover msg = 'Distribution must not be a developmental release to upload.' remote = False sys.argv = [sys.argv[0]] return local, remote, clear, upgrade, scripts, msg
[docs] def main() -> None: """Script entry point.""" *args, scripts, msg = handle_args() installer = SetupScripts if scripts else SetupPywikibot done = installer(*args).run() if msg: warning(f'<<lightred>>{msg}<<default>>') return done
if __name__ == '__main__': if not main(): sys.exit(1) # pragma: no cover