Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
flit / init.py
Size: Mime:
from datetime import date
import json
import os
from pathlib import Path
import re
import sys
import tomli_w

def get_data_dir():
    """Get the directory path for flit user data files.
    """
    home = os.path.realpath(os.path.expanduser('~'))

    if sys.platform == 'darwin':
        d = Path(home, 'Library')
    elif os.name == 'nt':
        appdata = os.environ.get('APPDATA', None)
        if appdata:
            d = Path(appdata)
        else:
            d = Path(home, 'AppData', 'Roaming')
    else:
        # Linux, non-OS X Unix, AIX, etc.
        xdg = os.environ.get("XDG_DATA_HOME", None)
        d = Path(xdg) if xdg else Path(home, '.local/share')

    return d / 'flit'

def get_defaults():
    try:
        with (get_data_dir() / 'init_defaults.json').open(encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

def store_defaults(d):
    data_dir = get_data_dir()
    try:
        data_dir.mkdir(parents=True)
    except FileExistsError:
        pass
    with (data_dir / 'init_defaults.json').open('w', encoding='utf-8') as f:
        json.dump(d, f, indent=2)

license_choices = [
    ('mit', "MIT - simple and permissive"),
    ('apache', "Apache - explicitly grants patent rights"),
    ('gpl3', "GPL - ensures that code based on this is shared with the same terms"),
    ('skip', "Skip - choose a license later"),
]

license_names_to_classifiers = {
    'mit': 'License :: OSI Approved :: MIT License',
    'gpl3': 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
    'apache': 'License :: OSI Approved :: Apache Software License'
}

license_templates_dir = Path(__file__).parent / 'license_templates'

class IniterBase:
    def __init__(self, directory='.'):
        self.directory = Path(directory)
        self.defaults = get_defaults()

    def validate_email(self, s):
        # Properly validating an email address is much more complex
        return bool(re.match(r'.+@.+', s)) or s == ""

    def validate_homepage(self, s):
        return not s or s.startswith(('http://', 'https://'))

    def guess_module_name(self):
        packages, modules = [], []
        for p in self.directory.iterdir():
            if not p.stem.isidentifier():
                continue

            if p.is_dir() and (p / '__init__.py').is_file():
                if p.name not in {'test', 'tests'}:
                    packages.append(p.name)

            elif p.is_file() and p.suffix == '.py':
                if p.stem not in {'setup'} and not p.name.startswith('test_'):
                    modules.append(p.stem)

        src_dir = self.directory / 'src'
        if src_dir.is_dir():
            for p in src_dir.iterdir():
                if not p.stem.isidentifier():
                    continue

                if p.is_dir() and (p / '__init__.py').is_file():
                    if p.name not in {'test', 'tests'}:
                        packages.append(p.name)

                elif p.is_file() and p.suffix == '.py':
                    if p.stem not in {'setup'} and not p.name.startswith('test_'):
                        modules.append(p.stem)

        if len(packages) == 1:
            return packages[0]
        elif len(packages) == 0 and len(modules) == 1:
            return modules[0]
        else:
            return None

    def update_defaults(self, author, author_email, module, home_page, license):
        new_defaults = {'author': author, 'author_email': author_email,
                        'license': license}
        name_chunk_pat = r'\b{}\b'.format(re.escape(module))
        if re.search(name_chunk_pat, home_page):
            new_defaults['home_page_template'] = \
                re.sub(name_chunk_pat, '{modulename}', home_page, flags=re.I)
        if any(new_defaults[k] != self.defaults.get(k) for k in new_defaults):
            self.defaults.update(new_defaults)
            store_defaults(self.defaults)

    def write_license(self, name, author):
        if (self.directory / 'LICENSE').exists():
            return
        year = date.today().year
        with (license_templates_dir / name).open(encoding='utf-8') as f:
            license_text = f.read()

        with (self.directory / 'LICENSE').open('w', encoding='utf-8') as f:
            f.write(license_text.format(year=year, author=author))

    def find_readme(self):
        allowed = ("readme.md","readme.rst","readme.txt")
        for fl in self.directory.glob("*.*"):
            if fl.name.lower() in allowed:
                return fl.name
        return None


class TerminalIniter(IniterBase):
    def prompt_text(self, prompt, default, validator, retry_msg="Try again."):
        if default is not None:
            p = "{} [{}]: ".format(prompt, default)
        else:
            p = prompt + ': '
        while True:
            response = input(p)
            if response == '' and default is not None:
                response = default
            if validator(response):
                return response

            print(retry_msg)

    def prompt_options(self, prompt, options, default=None):
        default_ix = None

        print(prompt)
        for i, (key, text) in enumerate(options, start=1):
            print("{}. {}".format(i, text))
            if key == default:
                default_ix = i

        while True:
            p = "Enter 1-" + str(len(options))
            if default_ix is not None:
                p += ' [{}]'.format(default_ix)
            response = input(p+': ')
            if (default_ix is not None) and response == '':
                return default

            if response.isnumeric():
                ir = int(response)
                if 1 <= ir <= len(options):
                    return options[ir-1][0]
            print("Try again.")

    def initialise(self):
        if (self.directory / 'pyproject.toml').exists():
            resp = input("pyproject.toml exists - overwrite it? [y/N]: ")
            if (not resp) or resp[0].lower() != 'y':
                return

        module = self.prompt_text('Module name', self.guess_module_name(),
                                  str.isidentifier)
        author = self.prompt_text('Author', self.defaults.get('author'),
                                  lambda s: True)
        author_email = self.prompt_text('Author email',
                        self.defaults.get('author_email'), self.validate_email)
        if 'home_page_template' in self.defaults:
            home_page_default = self.defaults['home_page_template'].replace(
                                                        '{modulename}', module)
        else:
            home_page_default = None
        home_page = self.prompt_text('Home page', home_page_default, self.validate_homepage,
                                     retry_msg="Should start with http:// or https:// - try again.")
        license = self.prompt_options('Choose a license (see http://choosealicense.com/ for more info)',
                    license_choices, self.defaults.get('license'))

        readme = self.find_readme()

        self.update_defaults(author=author, author_email=author_email,
                             home_page=home_page, module=module, license=license)

        # Format information as TOML
        # This is ugly code, but I want the generated pyproject.toml, which
        # will mostly be edited by hand, to look a particular way - e.g. authors
        # in inline tables. It's easier to 'cheat' with some string formatting
        # than to do this through a TOML library.
        author_info = []
        if author:
            author_info.append(f'name = {json.dumps(author, ensure_ascii=False)}')
        if author_email:
            author_info.append(f'email = {json.dumps(author_email)}')
        if author_info:
            authors_list = "[{%s}]" % ", ".join(author_info)
        else:
            authors_list = "[]"

        classifiers = []
        if license != 'skip':
            classifiers = [license_names_to_classifiers[license]]
            self.write_license(license, author)

        with (self.directory / 'pyproject.toml').open('w', encoding='utf-8') as f:
            f.write(TEMPLATE.format(
                name=json.dumps(module), authors=authors_list
            ))
            if readme:
                f.write(tomli_w.dumps({'readme': readme}))
            if license != 'skip':
                f.write('license = {file = "LICENSE"}\n')
            if classifiers:
                f.write(f"classifiers = {json.dumps(classifiers)}\n")
            f.write('dynamic = ["version", "description"]\n')
            if home_page:
                f.write("\n" + tomli_w.dumps({
                    'project': {'urls': {'Home': home_page}}
                }))

        print()
        print("Written pyproject.toml; edit that file to add optional extra info.")

TEMPLATE = """\
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[project]
name = {name}
authors = {authors}
"""

if __name__ == '__main__':
    TerminalIniter().initialise()