Skip to content

Instantly share code, notes, and snippets.

@xaedes
Last active October 7, 2022 13:36
Show Gist options
  • Save xaedes/27dee467f0ebf40fa2e38b0760f42299 to your computer and use it in GitHub Desktop.
Save xaedes/27dee467f0ebf40fa2e38b0760f42299 to your computer and use it in GitHub Desktop.
a python script which accepts command line arguments for package name to create folders and files for empty pip installable python package with that name.
#-*- mode: python -*-
# -*- coding: utf-8 -*-
#
# made 85% by copilot.
# copilot-mk-pip-package.py
# ---
# a python script which accepts command line arguments for package name to create folders and files for empty pip installable python package with that name.
#
# 1. creates the directory structure for a python package
# 2. creates the setup.py file
# 3. creates the README.md file
# 4. creates the __init__.py file
import os
import re
import sys
import argparse
# define required arguments. show help if arguments are not provided.
parser = argparse.ArgumentParser(description='Create a new pip installable python package')
parser.add_argument('package_name', help='Name of the package')
parser.add_argument('--version', help='Version of the package', default='0.0.1')
parser.add_argument('--author_name', help='Name of the author', default='Anonymous')
parser.add_argument('--author_email', help='Email of the author', default='')
parser.add_argument('--tests_folder', help='Name of the tests folder', default='tests')
parser.add_argument('--requirements_file', help='Name of the requirements file', default='requirements.txt')
parser.add_argument('--license', help='License of the package', default='MIT')
args = parser.parse_args()
# sanitize package name by replacing spaces and dashes with underscores and converting to lowercase.
args.package_name = args.package_name.replace(' ', '_').replace('-', '_').lower()
# function to remove indentations from the second line and following lines as indicated by the indentation of the second line, but keeps remaining indentation intact.
def remove_indent(text):
lines = text.splitlines()
if len(lines) < 2:
return text
indent = len(lines[1]) - len(lines[1].lstrip())
return '\n'.join([lines[0]] + [line[indent:] for line in lines[1:]])
# function to write into file, accepting multiline strings that have additional indentation because they are indented as part of the code.
# using remove_indent we can use multiline strings correctly indented in code, which will then be written to file, but
# the suplerflous indentation is removed by remove_indent.
def write_dedented(file, text):
file.write(remove_indent(text))
# print the arguments provided in a pretty format so that the user can verify that the arguments are correct and be hyped about the soon to be new package.
# use python ascii art generator library "art" to generate the ascii art of the new package and print it.
# add a fancy line to separate the arguments from the rest of the output.
import art
print(art.text2art(args.package_name))
print(remove_indent('''
Arguments provided:
'''))
# print args with values aligned by padding spaces
print('package_name'.ljust(20), args.package_name)
print('version'.ljust(20), args.version)
print('author_name'.ljust(20), args.author_name)
print('author_email'.ljust(20), args.author_email)
print('tests_folder'.ljust(20), args.tests_folder)
print('requirements_file'.ljust(20), args.requirements_file)
print('license'.ljust(20), args.license)
print('\n' + '-'*80 + '\n')
package_name = args.package_name
print("Creating package {}".format(package_name))
os.makedirs(package_name, exist_ok=True)
os.makedirs(os.path.join(package_name, package_name), exist_ok=True)
os.makedirs(os.path.join(package_name, args.tests_folder), exist_ok=True)
with open(os.path.join(package_name, "README.md"), "w") as f:
write_dedented(f, "# {}".format(package_name))
with open(os.path.join(package_name, "setup.py"), "w") as f:
# write the setup.py file using the provided arguments.
# find_package is used to find the packages from setup.py, but tests are excluded.
# find scripts from scripts folder using glob and specify them in setup call.
write_dedented(f, """
from setuptools import setup, find_packages
import glob, os
setup(
name='{package_name}',
version='{version}',
author='{author_name}',
author_email='{author_email}',
license='{license}',
packages=find_packages(exclude=['{tests_folder}']),
install_requires=[],
entry_points={{'console_scripts': ['{package_name}={package_name}.main:main']}},
)
""".format(
package_name=package_name, version=args.version, author_name=args.author_name,
author_email=args.author_email, tests_folder=args.tests_folder, license=args.license
)
)
# generate package __init__.py file defining version specified in args.version
with open(os.path.join(package_name, package_name, "__init__.py"), "w") as f:
write_dedented(f, '''__version__ = '{}'
'''.format(args.version))
# generate package main.py file with a hello world function
with open(os.path.join(package_name, package_name, "main.py"), "w") as f:
write_dedented(f, '''
def main():
print("Hello World!")
''')
# generate tests __init__.py file
with open(os.path.join(package_name, "tests", "__init__.py"), "w") as f:
write_dedented(f, "")
# generate example tests file using pytest, testing the addition of two numbers
with open(os.path.join(package_name, "tests", "test_add.py"), "w") as f:
write_dedented(f, '''
def test_add():
assert(1+2 == 3)
''')
# generate requirements.txt file
with open(os.path.join(package_name, args.requirements_file), "w") as f:
write_dedented(f, "")
# generate .gitignore file
with open(os.path.join(package_name, ".gitignore"), "w") as f:
write_dedented(f, """
*.egg*
*.pyc*
__pycache__
""")
# generate License file.
# downloading the license json as specified in args.license from github and parse it.
# write the license text to the license file.
# if license is not found or not specified in args.license, generate a "all rights reserved" license file instead.
all_rights_reserved = False
try:
import requests
import json
resp = requests.get('https://api.github.com/licenses/{}'.format(args.license.lower()))
if resp.status_code == 200:
license = json.loads(resp.text)
license_text = license['body']
# replace the '[year]' with current year and replace the '[fullname]' with args.author_name.
import datetime
license_text = license_text.replace('[year]', str(datetime.datetime.now().year))
license_text = license_text.replace('[fullname]', args.author_name)
with open(os.path.join(package_name, "LICENSE"), "w") as f:
write_dedented(f, license_text)
except:
all_rights_reserved = True
with open(os.path.join(package_name, "LICENSE"), "w") as f:
write_dedented(f, '''
All rights reserved
''')
# print out the name of the license, or "all rights reserved" if it applies.
print("License: {}".format(args.license if not all_rights_reserved else "all rights reserved"))
print("Package {} created".format(package_name))
# inform user on console how to install package with pip in dev mode (-e),
# how to run the tests, how to run the package main script and how to import
# the package from python.
print(remove_indent("""
To install the package in dev mode, run:
pip install -e {package_name}
To run the tests, run:
pytest {package_name}/tests
To run the package main script, run:
{package_name}
To import the package from python, run:
import {package_name}
""".format(package_name=package_name)))
# Now we can use this script to create a package with the name we want.
#
# $ python .\copilot-mk-pip-package.py banana_pie
# _ _
# | |__ __ _ _ __ __ _ _ __ __ _ _ __ (_) ___
# | '_ \ / _` || '_ \ / _` || '_ \ / _` | | '_ \ | | / _ \
# | |_) || (_| || | | || (_| || | | || (_| | | |_) || || __/
# |_.__/ \__,_||_| |_| \__,_||_| |_| \__,_| _____ | .__/ |_| \___|
# |_____||_|
#
# Arguments provided:
#
# package_name: banana_pie
# version: 0.0.1
# author_name: Anonymous
# author_email: scripts
# tests_folder: tests
# scripts_folder: scripts
# requirements_file: requirements.txt
# license: MIT
#
# --------------------------------------------------------------------------------
#
# Creating package banana_pie
# License: MIT
# Package banana_pie created
#
# $ tree banana_pie/
# banana_pie/
# ├── banana_pie
# │   ├── __init__.py
# │   └── main.py
# ├── README.md
# ├── LICENSE
# ├── setup.py
# ├── tests
# │ ├── __init__.py
# │ └── test_add.py
#
#
# And we have a package with the name we want.
#
# $ pip install -e banana_pie/
# Obtaining file:///home/rohit/banana_pie
# Installing collected packages: banana_pie
# Running setup.py develop for banana_pie
# Successfully installed banana_pie-0.0.1
#
# Execute the tests using pytests by running the following command:
# $ pytest banana_pie/tests/
#
# $ python
# Python 3.7.4 (default, Aug 13 2019, 20:35:49)
# [GCC 9.2.0] on linux
# Type "help", "copyright", "credits" or "license" for more information.
# >>> import banana_pie
# >>> banana_pie.__version__
# '0.1.0'
# >>>
#
# The package is installed in editable mode, so any changes we make to the package will be reflected immediately.
#
# $ echo "print('hello world')" >> banana_pie/banana_pie/__init__.py
#
# $ python
# Python 3.7.4 (default, Aug 13 2019, 20:35:49)
# [GCC 9.2.0] on linux
# Type "help", "copyright", "credits" or "license" for more information.
# >>> import banana_pie
# hello world
# >>>
#
# If we want to publish this package to pypi, we can use the following command:
# $ cd banana_pie/
# $ python setup.py sdist bdist_wheel
#
# This will generate a dist folder with the package and wheel files.
#
# $ tree dist/
# dist/
# ├── banana_pie-0.0.1-py3-none-any.whl
# └── banana_pie-0.0.1.tar.gz
#
# We can upload this to pypi using the twine package.
#
# $ twine upload dist/*
#
# $ pip install banana_pie
# Collecting banana_pie
# Downloading https://files.pythonhosted.org/packages/64/6f/6d0f8f8e5f7f0a5a9a7c9c8f3c7a5d0f3d2e3e3a5c5b5f5b5f7d5d5a5e7c/banana_pie-0.0.1-py3-none-any.whl
# Installing collected packages: banana-pie
# Successfully installed banana-pie-0.0.1
#
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment