install -c

Signed-off-by: Kenneth Reitz <me@kennethreitz.org>
This commit is contained in:
2017-09-23 21:00:43 -04:00
parent b953eac33a
commit 692387f70e
13 changed files with 4024 additions and 1 deletions
+19 -1
View File
@@ -10,6 +10,7 @@ import tempfile
from glob import glob
import json as simplejson
import background
import click
import click_completion
@@ -24,6 +25,7 @@ import pipdeptree
import requirements
import semver
from pipreqs import pipreqs
from blindspin import spinner
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from pip.req.req_file import parse_requirements
@@ -271,6 +273,10 @@ def ensure_environment():
)
def import_from_code(path='.'):
return pipreqs.get_all_imports(path, encoding='utf-8')
def ensure_pipfile(validate=True):
"""Creates a Pipfile for the project, if it doesn't exist."""
@@ -309,6 +315,11 @@ def ensure_pipfile(validate=True):
python = which('python') if not USING_DEFAULT_PYTHON else False
project.create_pipfile(python=python)
click.echo(crayons.white(u'Discovering imports from local codebase…', bold=True))
for req in import_from_code('.'):
click.echo(' Found {0}!'.format(crayons.green(req)))
project.add_package_to_pipfile(req)
# Validate the Pipfile's contents.
if validate and project.virtualenv_exists and not PIPENV_SKIP_VALIDATION:
# Ensure that Pipfile is using proper casing.
@@ -1571,6 +1582,7 @@ def do_py(system=False):
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
@click.option('--system', is_flag=True, default=False, help="System pip management.")
@click.option('--requirements', '-r', nargs=1, default=False, help="Import a requirements.txt file.")
@click.option('--code', '-c', nargs=1, default=False, help="Import from codebase.")
@click.option('--verbose', is_flag=True, default=False, help="Verbose mode.")
@click.option('--ignore-pipfile', is_flag=True, default=False, help="Ignore Pipfile when installing, using the Pipfile.lock.")
@click.option('--sequential', is_flag=True, default=False, help="Install dependencies one-at-a-time, isntead of concurrently.")
@@ -1580,7 +1592,7 @@ def install(
package_name=False, more_packages=False, dev=False, three=False,
python=False, system=False, lock=True, ignore_pipfile=False,
skip_lock=False, verbose=False, requirements=False, sequential=False,
pre=False
pre=False, code=False
):
# Automatically use an activated virtualenv.
@@ -1596,6 +1608,12 @@ def install(
click.echo(crayons.white(u'Requirements file provided! Importing into Pipfile…', bold=True), err=True)
import_requirements(r=requirements, dev=dev)
if code:
click.echo(crayons.white(u'Discovering imports from local codebase…', bold=True))
for req in import_from_code(code):
click.echo(' Found {0}!'.format(crayons.green(req)))
project.add_package_to_pipfile(req)
# Capture -e argument and assign it to following package_name.
more_packages = list(more_packages)
if package_name == '-e':
+581
View File
@@ -0,0 +1,581 @@
"""Pythonic command-line interface parser that will make you smile.
* http://docopt.org
* Repository and issue-tracker: https://github.com/docopt/docopt
* Licensed under terms of MIT license (see LICENSE-MIT)
* Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
"""
import sys
import re
__all__ = ['docopt']
__version__ = '0.6.2'
class DocoptLanguageError(Exception):
"""Error in construction of usage-message by developer."""
class DocoptExit(SystemExit):
"""Exit in case user invoked program with incorrect arguments."""
usage = ''
def __init__(self, message=''):
SystemExit.__init__(self, (message + '\n' + self.usage).strip())
class Pattern(object):
def __eq__(self, other):
return repr(self) == repr(other)
def __hash__(self):
return hash(repr(self))
def fix(self):
self.fix_identities()
self.fix_repeating_arguments()
return self
def fix_identities(self, uniq=None):
"""Make pattern-tree tips point to same object if they are equal."""
if not hasattr(self, 'children'):
return self
uniq = list(set(self.flat())) if uniq is None else uniq
for i, child in enumerate(self.children):
if not hasattr(child, 'children'):
assert child in uniq
self.children[i] = uniq[uniq.index(child)]
else:
child.fix_identities(uniq)
def fix_repeating_arguments(self):
"""Fix elements that should accumulate/increment values."""
either = [list(child.children) for child in transform(self).children]
for case in either:
for e in [child for child in case if case.count(child) > 1]:
if type(e) is Argument or type(e) is Option and e.argcount:
if e.value is None:
e.value = []
elif type(e.value) is not list:
e.value = e.value.split()
if type(e) is Command or type(e) is Option and e.argcount == 0:
e.value = 0
return self
def transform(pattern):
"""Expand pattern into an (almost) equivalent one, but with single Either.
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
Quirks: [-a] => (-a), (-a...) => (-a -a)
"""
result = []
groups = [[pattern]]
while groups:
children = groups.pop(0)
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore]
if any(t in map(type, children) for t in parents):
child = [c for c in children if type(c) in parents][0]
children.remove(child)
if type(child) is Either:
for c in child.children:
groups.append([c] + children)
elif type(child) is OneOrMore:
groups.append(child.children * 2 + children)
else:
groups.append(child.children + children)
else:
result.append(children)
return Either(*[Required(*e) for e in result])
class LeafPattern(Pattern):
"""Leaf/terminal node of a pattern tree."""
def __init__(self, name, value=None):
self.name, self.value = name, value
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
def flat(self, *types):
return [self] if not types or type(self) in types else []
def match(self, left, collected=None):
collected = [] if collected is None else collected
pos, match = self.single_match(left)
if match is None:
return False, left, collected
left_ = left[:pos] + left[pos + 1:]
same_name = [a for a in collected if a.name == self.name]
if type(self.value) in (int, list):
if type(self.value) is int:
increment = 1
else:
increment = ([match.value] if type(match.value) is str
else match.value)
if not same_name:
match.value = increment
return True, left_, collected + [match]
same_name[0].value += increment
return True, left_, collected
return True, left_, collected + [match]
class BranchPattern(Pattern):
"""Branch/inner node of a pattern tree."""
def __init__(self, *children):
self.children = list(children)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(repr(a) for a in self.children))
def flat(self, *types):
if type(self) in types:
return [self]
return sum([child.flat(*types) for child in self.children], [])
class Argument(LeafPattern):
def single_match(self, left):
for n, pattern in enumerate(left):
if type(pattern) is Argument:
return n, Argument(self.name, pattern.value)
return None, None
@classmethod
def parse(class_, source):
name = re.findall('(<\S*?>)', source)[0]
value = re.findall('\[default: (.*)\]', source, flags=re.I)
return class_(name, value[0] if value else None)
class Command(Argument):
def __init__(self, name, value=False):
self.name, self.value = name, value
def single_match(self, left):
for n, pattern in enumerate(left):
if type(pattern) is Argument:
if pattern.value == self.name:
return n, Command(self.name, True)
else:
break
return None, None
class Option(LeafPattern):
def __init__(self, short=None, long=None, argcount=0, value=False):
assert argcount in (0, 1)
self.short, self.long, self.argcount = short, long, argcount
self.value = None if value is False and argcount else value
@classmethod
def parse(class_, option_description):
short, long, argcount, value = None, None, 0, False
options, _, description = option_description.strip().partition(' ')
options = options.replace(',', ' ').replace('=', ' ')
for s in options.split():
if s.startswith('--'):
long = s
elif s.startswith('-'):
short = s
else:
argcount = 1
if argcount:
matched = re.findall('\[default: (.*)\]', description, flags=re.I)
value = matched[0] if matched else None
return class_(short, long, argcount, value)
def single_match(self, left):
for n, pattern in enumerate(left):
if self.name == pattern.name:
return n, pattern
return None, None
@property
def name(self):
return self.long or self.short
def __repr__(self):
return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
self.argcount, self.value)
class Required(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
l = left
c = collected
for pattern in self.children:
matched, l, c = pattern.match(l, c)
if not matched:
return False, left, collected
return True, l, c
class Optional(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
for pattern in self.children:
m, left, collected = pattern.match(left, collected)
return True, left, collected
class OptionsShortcut(Optional):
"""Marker/placeholder for [options] shortcut."""
class OneOrMore(BranchPattern):
def match(self, left, collected=None):
assert len(self.children) == 1
collected = [] if collected is None else collected
l = left
c = collected
l_ = None
matched = True
times = 0
while matched:
# could it be that something didn't match but changed l or c?
matched, l, c = self.children[0].match(l, c)
times += 1 if matched else 0
if l_ == l:
break
l_ = l
if times >= 1:
return True, l, c
return False, left, collected
class Either(BranchPattern):
def match(self, left, collected=None):
collected = [] if collected is None else collected
outcomes = []
for pattern in self.children:
matched, _, _ = outcome = pattern.match(left, collected)
if matched:
outcomes.append(outcome)
if outcomes:
return min(outcomes, key=lambda outcome: len(outcome[1]))
return False, left, collected
class Tokens(list):
def __init__(self, source, error=DocoptExit):
self += source.split() if hasattr(source, 'split') else source
self.error = error
@staticmethod
def from_pattern(source):
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
return Tokens(source, error=DocoptLanguageError)
def move(self):
return self.pop(0) if len(self) else None
def current(self):
return self[0] if len(self) else None
def parse_long(tokens, options):
"""long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
long, eq, value = tokens.move().partition('=')
assert long.startswith('--')
value = None if eq == value == '' else value
similar = [o for o in options if o.long == long]
if tokens.error is DocoptExit and similar == []: # if no exact match
similar = [o for o in options if o.long and o.long.startswith(long)]
if len(similar) > 1: # might be simply specified ambiguously 2+ times?
raise tokens.error('%s is not a unique prefix: %s?' %
(long, ', '.join(o.long for o in similar)))
elif len(similar) < 1:
argcount = 1 if eq == '=' else 0
o = Option(None, long, argcount)
options.append(o)
if tokens.error is DocoptExit:
o = Option(None, long, argcount, value if argcount else True)
else:
o = Option(similar[0].short, similar[0].long,
similar[0].argcount, similar[0].value)
if o.argcount == 0:
if value is not None:
raise tokens.error('%s must not have an argument' % o.long)
else:
if value is None:
if tokens.current() in [None, '--']:
raise tokens.error('%s requires argument' % o.long)
value = tokens.move()
if tokens.error is DocoptExit:
o.value = value if value is not None else True
return [o]
def parse_shorts(tokens, options):
"""shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
token = tokens.move()
assert token.startswith('-') and not token.startswith('--')
left = token.lstrip('-')
parsed = []
while left != '':
short, left = '-' + left[0], left[1:]
similar = [o for o in options if o.short == short]
if len(similar) > 1:
raise tokens.error('%s is specified ambiguously %d times' %
(short, len(similar)))
elif len(similar) < 1:
o = Option(short, None, 0)
options.append(o)
if tokens.error is DocoptExit:
o = Option(short, None, 0, True)
else: # why copying is necessary here?
o = Option(short, similar[0].long,
similar[0].argcount, similar[0].value)
value = None
if o.argcount != 0:
if left == '':
if tokens.current() in [None, '--']:
raise tokens.error('%s requires argument' % short)
value = tokens.move()
else:
value = left
left = ''
if tokens.error is DocoptExit:
o.value = value if value is not None else True
parsed.append(o)
return parsed
def parse_pattern(source, options):
tokens = Tokens.from_pattern(source)
result = parse_expr(tokens, options)
if tokens.current() is not None:
raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
return Required(*result)
def parse_expr(tokens, options):
"""expr ::= seq ( '|' seq )* ;"""
seq = parse_seq(tokens, options)
if tokens.current() != '|':
return seq
result = [Required(*seq)] if len(seq) > 1 else seq
while tokens.current() == '|':
tokens.move()
seq = parse_seq(tokens, options)
result += [Required(*seq)] if len(seq) > 1 else seq
return [Either(*result)] if len(result) > 1 else result
def parse_seq(tokens, options):
"""seq ::= ( atom [ '...' ] )* ;"""
result = []
while tokens.current() not in [None, ']', ')', '|']:
atom = parse_atom(tokens, options)
if tokens.current() == '...':
atom = [OneOrMore(*atom)]
tokens.move()
result += atom
return result
def parse_atom(tokens, options):
"""atom ::= '(' expr ')' | '[' expr ']' | 'options'
| long | shorts | argument | command ;
"""
token = tokens.current()
result = []
if token in '([':
tokens.move()
matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
result = pattern(*parse_expr(tokens, options))
if tokens.move() != matching:
raise tokens.error("unmatched '%s'" % token)
return [result]
elif token == 'options':
tokens.move()
return [OptionsShortcut()]
elif token.startswith('--') and token != '--':
return parse_long(tokens, options)
elif token.startswith('-') and token not in ('-', '--'):
return parse_shorts(tokens, options)
elif token.startswith('<') and token.endswith('>') or token.isupper():
return [Argument(tokens.move())]
else:
return [Command(tokens.move())]
def parse_argv(tokens, options, options_first=False):
"""Parse command-line argument vector.
If options_first:
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
else:
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
"""
parsed = []
while tokens.current() is not None:
if tokens.current() == '--':
return parsed + [Argument(None, v) for v in tokens]
elif tokens.current().startswith('--'):
parsed += parse_long(tokens, options)
elif tokens.current().startswith('-') and tokens.current() != '-':
parsed += parse_shorts(tokens, options)
elif options_first:
return parsed + [Argument(None, v) for v in tokens]
else:
parsed.append(Argument(None, tokens.move()))
return parsed
def parse_defaults(doc):
defaults = []
for s in parse_section('options:', doc):
# FIXME corner case "bla: options: --foo"
_, _, s = s.partition(':') # get rid of "options:"
split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
options = [Option.parse(s) for s in split if s.startswith('-')]
defaults += options
return defaults
def parse_section(name, source):
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
re.IGNORECASE | re.MULTILINE)
return [s.strip() for s in pattern.findall(source)]
def formal_usage(section):
_, _, section = section.partition(':') # drop "usage:"
pu = section.split()
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
def extras(help, version, options, doc):
if help and any((o.name in ('-h', '--help')) and o.value for o in options):
print(doc.strip("\n"))
sys.exit()
if version and any(o.name == '--version' and o.value for o in options):
print(version)
sys.exit()
class Dict(dict):
def __repr__(self):
return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
def docopt(doc, argv=None, help=True, version=None, options_first=False):
"""Parse `argv` based on command-line interface described in `doc`.
`docopt` creates your command-line interface based on its
description that you pass as `doc`. Such description can contain
--options, <positional-argument>, commands, which could be
[optional], (required), (mutually | exclusive) or repeated...
Parameters
----------
doc : str
Description of your command-line interface.
argv : list of str, optional
Argument vector to be parsed. sys.argv[1:] is used if not
provided.
help : bool (default: True)
Set to False to disable automatic help on -h or --help
options.
version : any object
If passed, the object will be printed if --version is in
`argv`.
options_first : bool (default: False)
Set to True to require options precede positional arguments,
i.e. to forbid options and positional arguments intermix.
Returns
-------
args : dict
A dictionary, where keys are names of command-line elements
such as e.g. "--verbose" and "<path>", and values are the
parsed values of those elements.
Example
-------
>>> from docopt import docopt
>>> doc = '''
... Usage:
... my_program tcp <host> <port> [--timeout=<seconds>]
... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
... my_program (-h | --help | --version)
...
... Options:
... -h, --help Show this screen and exit.
... --baud=<n> Baudrate [default: 9600]
... '''
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
>>> docopt(doc, argv)
{'--baud': '9600',
'--help': False,
'--timeout': '30',
'--version': False,
'<host>': '127.0.0.1',
'<port>': '80',
'serial': False,
'tcp': True}
See also
--------
* For video introduction see http://docopt.org
* Full documentation is available in README.rst as well as online
at https://github.com/docopt/docopt#readme
"""
argv = sys.argv[1:] if argv is None else argv
usage_sections = parse_section('usage:', doc)
if len(usage_sections) == 0:
raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
if len(usage_sections) > 1:
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
DocoptExit.usage = usage_sections[0]
options = parse_defaults(doc)
pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
# [default] syntax for argument is disabled
#for a in pattern.flat(Argument):
# same_name = [d for d in arguments if d.name == a.name]
# if same_name:
# a.value = same_name[0].value
argv = parse_argv(Tokens(argv), list(options), options_first)
pattern_options = set(pattern.flat(Option))
for options_shortcut in pattern.flat(OptionsShortcut):
doc_options = parse_defaults(doc)
options_shortcut.children = list(set(doc_options) - pattern_options)
#if any_options:
# options_shortcut.children += [Option(o.short, o.long, o.argcount)
# for o in argv if type(o) is Option]
extras(help, version, argv, doc)
matched, left, collected = pattern.fix().match(argv)
if matched and left == []: # better error message if left?
return Dict((a.name, a.value) for a in (pattern.flat() + collected))
raise DocoptExit()
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
__author__ = 'Vadim Kravcenko'
__email__ = 'vadim.kravcenko@gmail.com'
__version__ = '0.4.8'
+1123
View File
File diff suppressed because it is too large Load Diff
+402
View File
@@ -0,0 +1,402 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""pipreqs - Generate pip requirements.txt file based on imports
Usage:
pipreqs [options] <path>
Options:
--use-local Use ONLY local package info instead of querying PyPI
--pypi-server <url> Use custom PyPi server
--proxy <url> Use Proxy, parameter will be passed to requests library. You can also just set the
environments parameter in your terminal:
$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="https://10.10.1.10:1080"
--debug Print debug information
--ignore <dirs>... Ignore extra directories, each separated by a comma
--encoding <charset> Use encoding parameter for file open
--savepath <file> Save the list of requirements in the given file
--print Output the list of requirements in the standard output
--force Overwrite existing requirements.txt
--diff <file> Compare modules in requirements.txt to project imports.
--clean <file> Clean up requirements.txt by removing modules that are not imported in project.
"""
from __future__ import print_function, absolute_import
import os
import sys
import re
import logging
import codecs
import ast
import traceback
from docopt import docopt
import requests
from yarg import json2package
from yarg.exceptions import HTTPError
from pipreqs import __version__
REGEXP = [
re.compile(r'^import (.+)$'),
re.compile(r'^from ((?!\.+).*?) import (?:.*)$')
]
if sys.version_info[0] > 2:
open_func = open
py2 = False
else:
open_func = codecs.open
py2 = True
py2_exclude = ["concurrent", "concurrent.futures"]
def get_all_imports(path, encoding=None, extra_ignore_dirs=None):
imports = set()
raw_imports = set()
candidates = []
ignore_errors = False
ignore_dirs = [".hg", ".svn", ".git", ".tox", "__pycache__", "env", "venv"]
if extra_ignore_dirs:
ignore_dirs_parsed = []
for e in extra_ignore_dirs:
ignore_dirs_parsed.append(os.path.basename(os.path.realpath(e)))
ignore_dirs.extend(ignore_dirs_parsed)
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d not in ignore_dirs]
candidates.append(os.path.basename(root))
files = [fn for fn in files if os.path.splitext(fn)[1] == ".py"]
candidates += [os.path.splitext(fn)[0] for fn in files]
for file_name in files:
with open_func(os.path.join(root, file_name), "r", encoding=encoding) as f:
contents = f.read()
try:
tree = ast.parse(contents)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for subnode in node.names:
raw_imports.add(subnode.name)
elif isinstance(node, ast.ImportFrom):
raw_imports.add(node.module)
except Exception as exc:
if ignore_errors:
traceback.print_exc(exc)
logging.warn("Failed on file: %s" % os.path.join(root, file_name))
continue
else:
logging.error("Failed on file: %s" % os.path.join(root, file_name))
raise exc
# Clean up imports
for name in [n for n in raw_imports if n]:
# Sanity check: Name could have been None if the import statement was as from . import X
# Cleanup: We only want to first part of the import.
# Ex: from django.conf --> django.conf. But we only want django as an import
cleaned_name, _, _ = name.partition('.')
imports.add(cleaned_name)
packages = set(imports) - set(set(candidates) & set(imports))
logging.debug('Found packages: {0}'.format(packages))
with open(join("stdlib"), "r") as f:
data = [x.strip() for x in f.readlines()]
data = [x for x in data if x not in py2_exclude] if py2 else data
return sorted(list(set(packages) - set(data)))
def filter_line(l):
return len(l) > 0 and l[0] != "#"
def generate_requirements_file(path, imports):
with open(path, "w") as out_file:
logging.debug('Writing {num} requirements: {imports} to {file}'.format(
num=len(imports),
file=path,
imports=", ".join([x['name'] for x in imports])
))
fmt = '{name}=={version}'
out_file.write('\n'.join(fmt.format(**item) if item['version'] else '{name}'.format(**item)
for item in imports) + '\n')
def output_requirements(imports):
logging.debug('Writing {num} requirements: {imports} to stdout'.format(
num=len(imports),
imports=", ".join([x['name'] for x in imports])
))
fmt = '{name}=={version}'
print('\n'.join(fmt.format(**item) if item['version'] else '{name}'.format(**item)
for item in imports))
def get_imports_info(imports, pypi_server="https://pypi.python.org/pypi/", proxy=None):
result = []
for item in imports:
try:
response = requests.get("{0}{1}/json".format(pypi_server, item), proxies=proxy)
if response.status_code == 200:
if hasattr(response.content, 'decode'):
data = json2package(response.content.decode())
else:
data = json2package(response.content)
elif response.status_code >= 300:
raise HTTPError(status_code=response.status_code,
reason=response.reason)
except HTTPError:
logging.debug(
'Package %s does not exist or network problems', item)
continue
result.append({'name': item, 'version': data.latest_release_id})
return result
def get_locally_installed_packages(encoding=None):
packages = {}
ignore = ["tests", "_tests", "egg", "EGG", "info"]
for path in sys.path:
for root, dirs, files in os.walk(path):
for item in files:
if "top_level" in item:
with open_func(os.path.join(root, item), "r", encoding=encoding) as f:
package = root.split(os.sep)[-1].split("-")
try:
package_import = f.read().strip().split("\n")
except:
continue
for i_item in package_import:
if ((i_item not in ignore) and
(package[0] not in ignore)):
version = None
if len(package) > 1:
version = package[1].replace(
".dist", "").replace(".egg", "")
packages[i_item] = {
'version': version,
'name': package[0]
}
return packages
def get_import_local(imports, encoding=None):
local = get_locally_installed_packages()
result = []
for item in imports:
if item.lower() in local:
result.append(local[item.lower()])
# removing duplicates of package/version
result_unique = [
dict(t)
for t in set([
tuple(d.items()) for d in result
])
]
return result_unique
def get_pkg_names(pkgs):
result = []
with open(join("mapping"), "r") as f:
data = [x.strip().split(":") for x in f.readlines()]
for pkg in pkgs:
toappend = pkg
for item in data:
if item[0] == pkg:
toappend = item[1]
break
if toappend not in result:
result.append(toappend)
return result
def get_name_without_alias(name):
if "import " in name:
match = REGEXP[0].match(name.strip())
if match:
name = match.groups(0)[0]
return name.partition(' as ')[0].partition('.')[0].strip()
def join(f):
return os.path.join(os.path.dirname(__file__), f)
def parse_requirements(file_):
"""Parse a requirements formatted file.
Traverse a string until a delimiter is detected, then split at said
delimiter, get module name by element index, create a dict consisting of
module:version, and add dict to list of parsed modules.
Args:
file_: File to parse.
Raises:
OSerror: If there's any issues accessing the file.
Returns:
tuple: The contents of the file, excluding comments.
"""
modules = []
delim = ["<", ">", "=", "!", "~"] # https://www.python.org/dev/peps/pep-0508/#complete-grammar
try:
f = open_func(file_, "r")
except OSError:
logging.error("Failed on file: {}".format(file_))
raise
else:
data = [x.strip() for x in f.readlines() if x != "\n"]
finally:
f.close()
data = [x for x in data if x[0].isalpha()]
for x in data:
if not any([y in x for y in delim]): # Check for modules w/o a specifier.
modules.append({"name": x, "version": None})
for y in x:
if y in delim:
module = x.split(y)
module_name = module[0]
module_version = module[-1].replace("=", "")
module = {"name": module_name, "version": module_version}
if module not in modules:
modules.append(module)
break
return modules
def compare_modules(file_, imports):
"""Compare modules in a file to imported modules in a project.
Args:
file_ (str): File to parse for modules to be compared.
imports (tuple): Modules being imported in the project.
Returns:
tuple: The modules not imported in the project, but do exist in the
specified file.
"""
modules = parse_requirements(file_)
imports = [imports[i]["name"] for i in range(len(imports))]
modules = [modules[i]["name"] for i in range(len(modules))]
modules_not_imported = set(modules) - set(imports)
return modules_not_imported
def diff(file_, imports):
"""Display the difference between modules in a file and imported modules."""
modules_not_imported = compare_modules(file_, imports)
logging.info("The following modules are in {} but do not seem to be imported: "
"{}".format(file_, ", ".join(x for x in modules_not_imported)))
def clean(file_, imports):
"""Remove modules that aren't imported in project from file."""
modules_not_imported = compare_modules(file_, imports)
re_remove = re.compile("|".join(modules_not_imported))
to_write = []
try:
f = open_func(file_, "r+")
except OSError:
logging.error("Failed on file: {}".format(file_))
raise
else:
for i in f.readlines():
if re_remove.match(i) is None:
to_write.append(i)
f.seek(0)
f.truncate()
for i in to_write:
f.write(i)
finally:
f.close()
logging.info("Successfully cleaned up requirements in " + file_)
def init(args):
encoding = args.get('--encoding')
extra_ignore_dirs = args.get('--ignore')
if extra_ignore_dirs:
extra_ignore_dirs = extra_ignore_dirs.split(',')
candidates = get_all_imports(args['<path>'],
encoding=encoding,
extra_ignore_dirs=extra_ignore_dirs)
candidates = get_pkg_names(candidates)
logging.debug("Found imports: " + ", ".join(candidates))
pypi_server = "https://pypi.python.org/pypi/"
proxy = None
if args["--pypi-server"]:
pypi_server = args["--pypi-server"]
if args["--proxy"]:
proxy = {'http': args["--proxy"], 'https': args["--proxy"]}
if args["--use-local"]:
logging.debug(
"Getting package information ONLY from local installation.")
imports = get_import_local(candidates, encoding=encoding)
else:
logging.debug("Getting packages information from Local/PyPI")
local = get_import_local(candidates, encoding=encoding)
# Get packages that were not found locally
difference = [x for x in candidates
if x.lower() not in [z['name'].lower() for z in local]]
imports = local + get_imports_info(difference,
proxy=proxy,
pypi_server=pypi_server)
path = (args["--savepath"] if args["--savepath"] else
os.path.join(args['<path>'], "requirements.txt"))
if args["--diff"]:
diff(args["--diff"], imports)
return
if args["--clean"]:
clean(args["--clean"], imports)
return
if not args["--print"] and not args["--savepath"] and not args["--force"] and os.path.exists(path):
logging.warning("Requirements.txt already exists, "
"use --force to overwrite it")
return
if args["--print"]:
output_requirements(imports)
logging.info("Successfully output requirements")
else:
generate_requirements_file(path, imports)
logging.info("Successfully saved requirements file in " + path)
def main(): # pragma: no cover
args = docopt(__doc__, version=__version__)
log_level = logging.DEBUG if args['--debug'] else logging.INFO
logging.basicConfig(level=log_level, format='%(levelname)s: %(message)s')
try:
init(args)
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
main() # pragma: no cover
Vendored Executable
+1076
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
__title__ = 'yarg'
__version__ = '0.1.9'
__author__ = 'Kura'
__email__ = 'kura@kura.io'
__url__ = 'https://yarg.readthedocs.org/'
__license__ = 'MIT'
__copyright__ = 'Copyright 2014 Kura'
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
yarg(1) -- A semi hard Cornish cheese, also queries PyPI
========================================================
Yarg is a PyPI client.
>>> import yarg
>>>
>>> package = yarg.get("yarg")
>>> package.name
u'yarg'
>>> package.author
Author(name=u'Kura', email=u'kura@kura.io')
>>>
>>> yarg.newest_packages()
[<Package yarg>, <Package gray>, <Package ragy>]
>>>
>>> yarg.latest_updated_packages()
[<Package yarg>, <Package gray>, <Package ragy>]
Full documentation is at <https://yarg.readthedocs.org>.
"""
from .client import get
from .exceptions import HTTPError
from .package import json2package
from .parse import (newest_packages, latest_updated_packages)
__all__ = ['get', 'HTTPError', 'json2package', 'newest_packages',
'latest_updated_packages', ]
+54
View File
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import requests
from .exceptions import HTTPError
from .package import json2package
def get(package_name, pypi_server="https://pypi.python.org/pypi/"):
"""
Constructs a request to the PyPI server and returns a
:class:`yarg.package.Package`.
:param package_name: case sensitive name of the package on the PyPI server.
:param pypi_server: (option) URL to the PyPI server.
>>> import yarg
>>> package = yarg.get('yarg')
<Package yarg>
"""
if not pypi_server.endswith("/"):
pypi_server = pypi_server + "/"
response = requests.get("{0}{1}/json".format(pypi_server,
package_name))
if response.status_code >= 300:
raise HTTPError(status_code=response.status_code,
reason=response.reason)
if hasattr(response.content, 'decode'):
return json2package(response.content.decode())
else:
return json2package(response.content)
+58
View File
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from requests.exceptions import HTTPError as RHTTPError
class YargException(Exception):
pass
class HTTPError(YargException, RHTTPError):
"""
A catchall HTTPError exception to handle HTTP errors
when using :meth:`yarg.get`.
This exception is also loaded at :class:`yarg.HTTPError`
for ease of access.
:member: status_code
"""
def __init__(self, *args, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
if hasattr(self, 'status_code'):
setattr(self, 'errno', self.status_code)
if hasattr(self, 'reason'):
setattr(self, 'message', self.reason)
def __str__(self):
return self.__repr__()
def __repr__(self):
if hasattr(self, 'status_code') and hasattr(self, 'reason'):
return "<HTTPError {0} {1}>".format(self.status_code, self.reason)
return "<HTTPError>"
+324
View File
@@ -0,0 +1,324 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
try:
import simplejson as json
except ImportError:
import json
from collections import namedtuple
import re
from .release import Release
class Package(object):
"""
A PyPI package.
:param pypi_dict: A dictionary retrieved from the PyPI server.
"""
def __init__(self, pypi_dict):
self._package = pypi_dict['info']
self._releases = pypi_dict['releases']
def __repr__(self):
return "<Package {0}>".format(self.name)
@property
def name(self):
"""
>>> package = yarg.get('yarg')
>>> package.name
u'yarg'
"""
return self._package['name']
@property
def pypi_url(self):
"""
>>> package = yarg.get('yarg')
>>> package.url
u'https://pypi.python.org/pypi/yarg'
"""
return self._package['package_url']
@property
def summary(self):
"""
>>> package = yarg.get('yarg')
>>> package.summary
u'Some random summary stuff'
"""
return self._package['summary']
@property
def description(self):
"""
>>> package = yarg.get('yarg')
>>> package.description
u'A super long description, usually uploaded from the README'
"""
return self._package['description']
@property
def homepage(self):
"""
>>> package = yarg.get('yarg')
>>> package.homepage
u'https://kura.io/yarg/'
"""
if ('home_page' not in self._package or
self._package['home_page'] == ""):
return None
return self._package['home_page']
@property
def bugtracker(self):
"""
>>> package = yarg.get('yarg')
>>> package.bugtracker
u'https://github.com/kura/yarg/issues'
"""
if ('bugtrack_url' not in self._package or
self._package['bugtrack_url'] == ""):
return None
return self._package['bugtrack_url']
@property
def docs(self):
"""
>>> package = yarg.get('yarg')
>>> package.docs
u'https://yarg.readthedocs.org/en/latest'
"""
if ('docs_url' not in self._package or
self._package['docs_url'] == ""):
return None
return self._package['docs_url']
@property
def author(self):
"""
>>> package = yarg.get('yarg')
>>> package.author
Author(name=u'Kura', email=u'kura@kura.io')
"""
author = namedtuple('Author', 'name email')
return author(name=self._package['author'],
email=self._package['author_email'])
@property
def maintainer(self):
"""
>>> package = yarg.get('yarg')
>>> package.maintainer
Maintainer(name=u'Kura', email=u'kura@kura.io')
"""
maintainer = namedtuple('Maintainer', 'name email')
return maintainer(name=self._package['maintainer'],
email=self._package['maintainer_email'])
@property
def license(self):
"""
>>> package = yarg.get('yarg')
>>> package.license
u'MIT'
"""
return self._package['license']
@property
def license_from_classifiers(self):
"""
>>> package = yarg.get('yarg')
>>> package.license_from_classifiers
u'MIT License'
"""
if len(self.classifiers) > 0:
for c in self.classifiers:
if c.startswith("License"):
return c.split(" :: ")[-1]
@property
def downloads(self):
"""
>>> package = yarg.get('yarg')
>>> package.downloads
Downloads(day=50100, week=367941, month=1601938) # I wish
"""
_downloads = self._package['downloads']
downloads = namedtuple('Downloads', 'day week month')
return downloads(day=_downloads['last_day'],
week=_downloads['last_week'],
month=_downloads['last_month'])
@property
def classifiers(self):
"""
>>> package = yarg.get('yarg')
>>> package.classifiers
[u'License :: OSI Approved :: MIT License',
u'Programming Language :: Python :: 2.7',
u'Programming Language :: Python :: 3.4']
"""
return self._package['classifiers']
@property
def python_versions(self):
"""
Returns a list of Python version strings that
the package has listed in :attr:`yarg.Release.classifiers`.
>>> package = yarg.get('yarg')
>>> package.python_versions
[u'2.6', u'2.7', u'3.3', u'3.4']
"""
version_re = re.compile(r"""Programming Language \:\: """
"""Python \:\: \d\.\d""")
return [c.split(' :: ')[-1] for c in self.classifiers
if version_re.match(c)]
@property
def python_implementations(self):
"""
Returns a list of Python implementation strings that
the package has listed in :attr:`yarg.Release.classifiers`.
>>> package = yarg.get('yarg')
>>> package.python_implementations
[u'CPython', u'PyPy']
"""
return [c.split(' :: ')[-1] for c in self.classifiers
if c.startswith("""Programming Language :: """
"""Python :: Implementation""")]
@property
def latest_release_id(self):
"""
>>> package = yarg.get('yarg')
>>> package.latest_release_id
u'0.1.0'
"""
return self._package['version']
@property
def latest_release(self):
"""
A list of :class:`yarg.release.Release` objects for each file in the
latest release.
>>> package = yarg.get('yarg')
>>> package.latest_release
[<Release 0.1.0>, <Release 0.1.0>]
"""
release_id = self.latest_release_id
return self.release(release_id)
@property
def has_wheel(self):
"""
Returns `True` if one of the :class:`yarg.release.Release` objects
in the latest set of release files is `wheel` format. Returns
`False` if not.
>>> package = yarg.get('yarg')
>>> package.has_wheel
True
"""
for release in self.latest_release:
if release.package_type in ('wheel', 'bdist_wheel'):
return True
return False
@property
def has_egg(self):
"""
Returns `True` if one of the :class:`yarg.release.Release` objects
in the latest set of release files is `egg` format. Returns
`False` if not.
>>> package = yarg.get('yarg')
>>> package.has_egg
False
"""
for release in self.latest_release:
if release.package_type in ('egg', 'bdist_egg'):
return True
return False
@property
def has_source(self):
"""
Returns `True` if one of the :class:`yarg.release.Release` objects
in the latest set of release files is `source` format. Returns
`False` if not.
>>> package = yarg.get('yarg')
>>> package.has_source
True
"""
for release in self.latest_release:
if release.package_type in ('source', 'sdist'):
return True
return False
@property
def release_ids(self):
"""
>>> package = yarg.get('yarg')
>>> package.release_ids
[u'0.0.1', u'0.0.5', u'0.1.0']
"""
r = [(k, self._releases[k][0]['upload_time'])
for k in self._releases.keys()
if len(self._releases[k]) > 0]
return [k[0] for k in sorted(r, key=lambda k: k[1])]
def release(self, release_id):
"""
A list of :class:`yarg.release.Release` objects for each file in a
release.
:param release_id: A pypi release id.
>>> package = yarg.get('yarg')
>>> last_release = yarg.releases[-1]
>>> package.release(last_release)
[<Release 0.1.0>, <Release 0.1.0>]
"""
if release_id not in self.release_ids:
return None
return [Release(release_id, r) for r in self._releases[release_id]]
def json2package(json_content):
"""
Returns a :class:`yarg.release.Release` object from JSON content from the
PyPI server.
:param json_content: JSON encoded content from the PyPI server.
"""
return Package(json.loads(json_content))
+172
View File
@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from datetime import datetime
import xml.etree.ElementTree
import requests
from .exceptions import HTTPError
def _get(pypi_server):
"""
Query the PyPI RSS feed and return a list
of XML items.
"""
response = requests.get(pypi_server)
if response.status_code >= 300:
raise HTTPError(status_code=response.status_code,
reason=response.reason)
if hasattr(response.content, 'decode'):
tree = xml.etree.ElementTree.fromstring(response.content.decode())
else:
tree = xml.etree.ElementTree.fromstring(response.content)
channel = tree.find('channel')
return channel.findall('item')
def newest_packages(
pypi_server="https://pypi.python.org/pypi?%3Aaction=packages_rss"):
"""
Constructs a request to the PyPI server and returns a list of
:class:`yarg.parse.Package`.
:param pypi_server: (option) URL to the PyPI server.
>>> import yarg
>>> yarg.newest_packages()
[<Package yarg>, <Package gray>, <Package ragy>]
"""
items = _get(pypi_server)
i = []
for item in items:
i_dict = {'name': item[0].text.split()[0],
'url': item[1].text,
'description': item[3].text,
'date': item[4].text}
i.append(Package(i_dict))
return i
def latest_updated_packages(
pypi_server="https://pypi.python.org/pypi?%3Aaction=rss"):
"""
Constructs a request to the PyPI server and returns a list of
:class:`yarg.parse.Package`.
:param pypi_server: (option) URL to the PyPI server.
>>> import yarg
>>> yarg.latest_updated_packages()
[<Package yarg>, <Package gray>, <Package ragy>]
"""
items = _get(pypi_server)
i = []
for item in items:
name, version = item[0].text.split()
i_dict = {'name': name,
'version': version,
'url': item[1].text,
'description': item[2].text,
'date': item[3].text}
i.append(Package(i_dict))
return i
class Package(object):
"""
A PyPI package generated from the RSS feed information.
:param pypi_dict: A dictionary retrieved from the PyPI server.
"""
def __init__(self, pypi_dict):
self._content = pypi_dict
def __repr__(self):
return "<Package {0}>".format(self.name)
@property
def name(self):
"""
>>> package = yarg.newest_packages()[0]
>>> package.name
u'yarg'
>>> package = yarg.latest_updated_packages()[0]
>>> package.name
u'yarg'
"""
return self._content['name']
@property
def version(self):
"""
>>> package = yarg.newest_packages()[0]
>>> package.name
u'yarg'
>>> package = yarg.latest_updated_packages()[0]
>>> package.name
u'yarg'
"""
if 'version' not in self._content:
return None
return self._content['version']
@property
def url(self):
"""
This is only available for :meth:`yarg.latest_updated_packages`, for
:meth:`yarg.newest_packages` will return `None`
>>> package = yarg.latest_updated_packages()[0]
>>> package.url
u'http://pypi.python.org/pypi/yarg'
"""
return self._content['url']
@property
def date(self):
"""
>>> package = yarg.newest_packages()[0]
>>> package.date
datetime.datetime(2014, 8, 9, 8, 40, 20)
>>> package = yarg.latest_updated_packages()[0]
>>> package.date
datetime.datetime(2014, 8, 9, 8, 40, 20)
"""
return datetime.strptime(self._content['date'],
"%d %b %Y %H:%M:%S %Z")
@property
def description(self):
"""
>>> package = yarg.newest_packages()[0]
>>> package.description
u'Some random summary stuff'
>>> package = yarg.latest_updated_packages()[0]
>>> package.description
u'Some random summary stuff'
"""
return self._content['description']
+147
View File
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# (The MIT License)
#
# Copyright (c) 2014 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from datetime import datetime
class Release(object):
"""
A release file from PyPI.
:param release_id: A release id.
:param pypi_dict: A dictionary of a release file.
"""
def __init__(self, release_id, pypi_dict):
self._release = pypi_dict
self._release['release_id'] = release_id
def __repr__(self):
return "<Release {0}>".format(self.release_id)
@property
def release_id(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r[0].release_id
u'0.1.0'
"""
return self._release['release_id']
@property
def uploaded(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.uploaded
datetime.datime(2014, 8, 7, 21, 26, 19)
"""
return datetime.strptime(self._release['upload_time'],
'%Y-%m-%dT%H:%M:%S')
@property
def python_version(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.python_version
u'2.7'
"""
return self._release['python_version']
@property
def url(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.url
u'https://pypi.python.org/packages/2.7/y/yarg/yarg...'
"""
return self._release['url']
@property
def md5_digest(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.md5_digest
u'bec88e1c1765ca6177360e8f37b44c5c'
"""
return self._release['md5_digest']
@property
def filename(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.filename
u'yarg-0.1.0-py27-none-any.whl'
"""
return self._release['filename']
@property
def size(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.size
52941
"""
return self._release['size']
@property
def package_type(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.package_type
u'wheel'
"""
mapping = {'bdist_egg': u'egg', 'bdist_wheel': u'wheel',
'sdist': u'source'}
ptype = self._release['packagetype']
if ptype in mapping.keys():
return mapping[ptype]
return ptype
@property
def has_sig(self):
"""
>>> package = yarg.get('yarg')
>>> v = "0.1.0"
>>> r = package.release(v)
>>> r.has_sig
True
"""
return self._release['has_sig']