зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1393242 - Vendor python-hglib 2.4; r=mshal
python-hglib is a Python client for Mercurial's command server. It facilitates querying Mercurial efficiently (using a single process) and without having to parse output in the common case. Let's vendor it so we can make use of it for more advanced Mercurial scenarios. Content vendored from changeset 820d7c1e470a without modifications (other than deleting unwanted files). As part of vendoring, we add the package to the virtualenv and make it available to mach. MozReview-Commit-ID: F4KLbW1lAvk --HG-- extra : rebase_source : 39321a880a13a0b0323a7217f538978b729e2afe
This commit is contained in:
Родитель
48ba6bac22
Коммит
258638f8d6
|
@ -9,6 +9,7 @@ mozilla.pth:third_party/python/configobj
|
|||
mozilla.pth:third_party/python/cram
|
||||
mozilla.pth:third_party/python/dlmanager
|
||||
mozilla.pth:third_party/python/futures
|
||||
mozilla.pth:third_party/python/hglib
|
||||
mozilla.pth:third_party/python/jsmin
|
||||
optional:setup.py:third_party/python/psutil:build_ext:--inplace
|
||||
mozilla.pth:third_party/python/psutil
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2011 Matt Mackall and other contributors
|
||||
|
||||
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.
|
|
@ -0,0 +1,40 @@
|
|||
import subprocess
|
||||
from hglib import client, util, error
|
||||
|
||||
HGPATH = 'hg'
|
||||
|
||||
def open(path=None, encoding=None, configs=None):
|
||||
'''starts a cmdserver for the given path (or for a repository found
|
||||
in the cwd). HGENCODING is set to the given encoding. configs is a
|
||||
list of key, value, similar to those passed to hg --config.
|
||||
'''
|
||||
return client.hgclient(path, encoding, configs)
|
||||
|
||||
def init(dest=None, ssh=None, remotecmd=None, insecure=False,
|
||||
encoding=None, configs=None):
|
||||
args = util.cmdbuilder('init', dest, e=ssh, remotecmd=remotecmd,
|
||||
insecure=insecure)
|
||||
|
||||
args.insert(0, HGPATH)
|
||||
proc = util.popen(args)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode:
|
||||
raise error.CommandError(args, proc.returncode, out, err)
|
||||
|
||||
return client.hgclient(dest, encoding, configs, connect=False)
|
||||
|
||||
def clone(source=None, dest=None, noupdate=False, updaterev=None, rev=None,
|
||||
branch=None, pull=False, uncompressed=False, ssh=None, remotecmd=None,
|
||||
insecure=False, encoding=None, configs=None):
|
||||
args = util.cmdbuilder('clone', source, dest, noupdate=noupdate,
|
||||
updaterev=updaterev, rev=rev, branch=branch,
|
||||
pull=pull, uncompressed=uncompressed,
|
||||
e=ssh, remotecmd=remotecmd, insecure=insecure)
|
||||
|
||||
args.insert(0, HGPATH)
|
||||
proc = util.popen(args)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode:
|
||||
raise error.CommandError(args, proc.returncode, out, err)
|
||||
|
||||
return client.hgclient(dest, encoding, configs, connect=False)
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,238 @@
|
|||
import hglib.client # Circular dependency.
|
||||
from hglib import util, templates
|
||||
from hglib.error import CommandError
|
||||
from hglib.util import b, strtobytes, integertypes
|
||||
|
||||
_nullcset = [b('-1'), b('0000000000000000000000000000000000000000'), b(''),
|
||||
b(''), b(''), b(''), b('')]
|
||||
|
||||
class changectx(object):
|
||||
"""A changecontext object makes access to data related to a particular
|
||||
changeset convenient."""
|
||||
def __init__(self, repo, changeid=b('')):
|
||||
"""changeid is a revision number, node, or tag"""
|
||||
if changeid == b(''):
|
||||
changeid = b('.')
|
||||
self._repo = repo
|
||||
if isinstance(changeid, hglib.client.revision):
|
||||
cset = changeid
|
||||
elif changeid == -1:
|
||||
cset = _nullcset
|
||||
else:
|
||||
if isinstance(changeid, integertypes):
|
||||
changeid = b('rev(') + strtobytes(changeid) + b(')')
|
||||
|
||||
notfound = False
|
||||
try:
|
||||
cset = self._repo.log(changeid)
|
||||
# hg bbf4f3dfd700 gave a null result for tip+1
|
||||
if (cset and cset[0][1] == _nullcset[1]
|
||||
and cset[0][0] != _nullcset[0]):
|
||||
notfound = True
|
||||
except CommandError:
|
||||
notfound = True
|
||||
|
||||
if notfound or not len(cset):
|
||||
raise ValueError('changeid %r not found in repo' % changeid)
|
||||
if len(cset) > 1:
|
||||
raise ValueError('changeid must yield a single changeset')
|
||||
cset = cset[0]
|
||||
|
||||
self._rev, self._node, self._tags = cset[:3]
|
||||
self._branch, self._author, self._description, self._date = cset[3:]
|
||||
|
||||
self._rev = int(self._rev)
|
||||
|
||||
self._tags = self._tags.split()
|
||||
try:
|
||||
self._tags.remove(b('tip'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self._ignored = None
|
||||
self._clean = None
|
||||
|
||||
def __str__(self):
|
||||
return self._node[:12].decode('latin-1')
|
||||
|
||||
def __int__(self):
|
||||
return self._rev
|
||||
|
||||
def __repr__(self):
|
||||
return "<changectx %s>" % str(self)
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
return hash(self._rev)
|
||||
except AttributeError:
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
return self._rev == other._rev
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __nonzero__(self):
|
||||
return self._rev != -1
|
||||
|
||||
def __bool__(self):
|
||||
return self.__nonzero__()
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._manifest
|
||||
|
||||
def __iter__(self):
|
||||
for f in sorted(self._manifest):
|
||||
yield f
|
||||
|
||||
@util.propertycache
|
||||
def _status(self):
|
||||
return self._parsestatus(self._repo.status(change=strtobytes(self)))[:4]
|
||||
|
||||
def _parsestatus(self, stat):
|
||||
d = dict((c, [])
|
||||
for c in (b('M'), b('A'), b('R'), b('!'), b('?'), b('I'),
|
||||
b('C'), b(' ')))
|
||||
for k, path in stat:
|
||||
d[k].append(path)
|
||||
return (d[b('M')], d[b('A')], d[b('R')], d[b('!')], d[b('?')],
|
||||
d[b('I')], d[b('C')])
|
||||
|
||||
def status(self, ignored=False, clean=False):
|
||||
"""Explicit status query
|
||||
Unless this method is used to query the working copy status, the
|
||||
_status property will implicitly read the status using its default
|
||||
arguments."""
|
||||
stat = self._parsestatus(self._repo.status(change=strtobytes(self),
|
||||
ignored=ignored,
|
||||
clean=clean))
|
||||
self._unknown = self._ignored = self._clean = None
|
||||
if ignored:
|
||||
self._ignored = stat[5]
|
||||
if clean:
|
||||
self._clean = stat[6]
|
||||
self._status = stat[:4]
|
||||
return stat
|
||||
|
||||
def rev(self):
|
||||
return self._rev
|
||||
|
||||
def node(self):
|
||||
return self._node
|
||||
|
||||
def tags(self):
|
||||
return self._tags
|
||||
|
||||
def branch(self):
|
||||
return self._branch
|
||||
|
||||
def author(self):
|
||||
return self._author
|
||||
|
||||
def user(self):
|
||||
return self._author
|
||||
|
||||
def date(self):
|
||||
return self._date
|
||||
|
||||
def description(self):
|
||||
return self._description
|
||||
|
||||
def files(self):
|
||||
return sorted(self._status[0] + self._status[1] + self._status[2])
|
||||
|
||||
def modified(self):
|
||||
return self._status[0]
|
||||
|
||||
def added(self):
|
||||
return self._status[1]
|
||||
|
||||
def removed(self):
|
||||
return self._status[2]
|
||||
|
||||
def ignored(self):
|
||||
if self._ignored is None:
|
||||
self.status(ignored=True)
|
||||
return self._ignored
|
||||
|
||||
def clean(self):
|
||||
if self._clean is None:
|
||||
self.status(clean=True)
|
||||
return self._clean
|
||||
|
||||
@util.propertycache
|
||||
def _manifest(self):
|
||||
d = {}
|
||||
for node, p, e, s, path in self._repo.manifest(rev=strtobytes(self)):
|
||||
d[path] = node
|
||||
return d
|
||||
|
||||
def manifest(self):
|
||||
return self._manifest
|
||||
|
||||
def hex(self):
|
||||
return hex(self._node)
|
||||
|
||||
@util.propertycache
|
||||
def _parents(self):
|
||||
"""return contexts for each parent changeset"""
|
||||
par = self._repo.parents(rev=strtobytes(self))
|
||||
if not par:
|
||||
return [changectx(self._repo, -1)]
|
||||
return [changectx(self._repo, int(cset.rev)) for cset in par]
|
||||
|
||||
def parents(self):
|
||||
return self._parents
|
||||
|
||||
def p1(self):
|
||||
return self._parents[0]
|
||||
|
||||
def p2(self):
|
||||
if len(self._parents) == 2:
|
||||
return self._parents[1]
|
||||
return changectx(self._repo, -1)
|
||||
|
||||
@util.propertycache
|
||||
def _bookmarks(self):
|
||||
books = [bm for bm in self._repo.bookmarks()[0] if bm[1] == self._rev]
|
||||
|
||||
bms = []
|
||||
for name, r, n in books:
|
||||
bms.append(name)
|
||||
return bms
|
||||
|
||||
def bookmarks(self):
|
||||
return self._bookmarks
|
||||
|
||||
def hidden(self):
|
||||
"""return True if the changeset is hidden, else False"""
|
||||
return bool(self._repo.log(revrange=self._node + b(' and hidden()'),
|
||||
hidden=True))
|
||||
|
||||
def phase(self):
|
||||
"""return the phase of the changeset (public, draft or secret)"""
|
||||
return self._repo.phase(strtobytes(self._rev))[0][1]
|
||||
|
||||
def children(self):
|
||||
"""return contexts for each child changeset"""
|
||||
for c in self._repo.log(b('children(') + self._node + b(')')):
|
||||
yield changectx(self._repo, c)
|
||||
|
||||
def ancestors(self):
|
||||
for a in self._repo.log(b('ancestors(') + self._node + b(')')):
|
||||
yield changectx(self._repo, a)
|
||||
|
||||
def descendants(self):
|
||||
for d in self._repo.log(b('descendants(') + self._node + b(')')):
|
||||
yield changectx(self._repo, d)
|
||||
|
||||
def ancestor(self, c2):
|
||||
"""
|
||||
return the ancestor context of self and c2
|
||||
"""
|
||||
return changectx(self._repo,
|
||||
b('ancestor(') + self + b(', ') + c2 + b(')'))
|
|
@ -0,0 +1,18 @@
|
|||
class CommandError(Exception):
|
||||
def __init__(self, args, ret, out, err):
|
||||
self.args = args
|
||||
self.ret = ret
|
||||
self.out = out
|
||||
self.err = err
|
||||
|
||||
def __str__(self):
|
||||
return str((self.ret, self.out.rstrip(), self.err.rstrip()))
|
||||
|
||||
class ServerError(Exception):
|
||||
pass
|
||||
|
||||
class ResponseError(ServerError, ValueError):
|
||||
pass
|
||||
|
||||
class CapabilityError(ServerError):
|
||||
pass
|
|
@ -0,0 +1,21 @@
|
|||
from hglib.util import b
|
||||
|
||||
class handlers(object):
|
||||
"""
|
||||
These can be used as the cb argument to hgclient.merge() to control the
|
||||
behaviour when Mercurial prompts what to do with regard to a specific file,
|
||||
e.g. when one parent modified a file and the other removed it.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def abort(size, output):
|
||||
"""
|
||||
Abort the merge if a prompt appears.
|
||||
"""
|
||||
return b('')
|
||||
|
||||
"""
|
||||
This corresponds to Mercurial's -y/--noninteractive global option, which
|
||||
picks the first choice on all prompts.
|
||||
"""
|
||||
noninteractive = 'yes'
|
|
@ -0,0 +1,4 @@
|
|||
from hglib.util import b
|
||||
|
||||
changeset = b('{rev}\\0{node}\\0{tags}\\0{branch}\\0{author}'
|
||||
'\\0{desc}\\0{date}\\0')
|
|
@ -0,0 +1,217 @@
|
|||
import os, subprocess, sys
|
||||
from hglib import error
|
||||
try:
|
||||
from io import BytesIO
|
||||
except ImportError:
|
||||
from cStringIO import StringIO as BytesIO
|
||||
|
||||
if sys.version_info[0] > 2:
|
||||
izip = zip
|
||||
integertypes = (int,)
|
||||
|
||||
def b(s):
|
||||
"""Encode the string as bytes."""
|
||||
return s.encode('latin-1')
|
||||
else:
|
||||
from itertools import izip
|
||||
integertypes = (long, int)
|
||||
bytes = str # Defined in Python 2.6/2.7, but to the same value.
|
||||
|
||||
def b(s):
|
||||
"""Encode the string as bytes."""
|
||||
return s
|
||||
|
||||
def strtobytes(s):
|
||||
"""Return the bytes of the string representation of an object."""
|
||||
return str(s).encode('latin-1')
|
||||
|
||||
def grouper(n, iterable):
|
||||
''' list(grouper(2, range(4))) -> [(0, 1), (2, 3)] '''
|
||||
args = [iter(iterable)] * n
|
||||
return izip(*args)
|
||||
|
||||
def eatlines(s, n):
|
||||
"""
|
||||
>>> eatlines(b("1\\n2"), 1) == b('2')
|
||||
True
|
||||
>>> eatlines(b("1\\n2"), 2) == b('')
|
||||
True
|
||||
>>> eatlines(b("1\\n2"), 3) == b('')
|
||||
True
|
||||
>>> eatlines(b("1\\n2\\n3"), 1) == b('2\\n3')
|
||||
True
|
||||
"""
|
||||
cs = BytesIO(s)
|
||||
|
||||
for line in cs:
|
||||
n -= 1
|
||||
if n == 0:
|
||||
return cs.read()
|
||||
return b('')
|
||||
|
||||
def skiplines(s, prefix):
|
||||
"""
|
||||
Skip lines starting with prefix in s
|
||||
|
||||
>>> skiplines(b('a\\nb\\na\\n'), b('a')) == b('b\\na\\n')
|
||||
True
|
||||
>>> skiplines(b('a\\na\\n'), b('a')) == b('')
|
||||
True
|
||||
>>> skiplines(b(''), b('a')) == b('')
|
||||
True
|
||||
>>> skiplines(b('a\\nb'), b('b')) == b('a\\nb')
|
||||
True
|
||||
"""
|
||||
cs = BytesIO(s)
|
||||
|
||||
for line in cs:
|
||||
if not line.startswith(prefix):
|
||||
return line + cs.read()
|
||||
|
||||
return b('')
|
||||
|
||||
def _cmdval(val):
|
||||
if isinstance(val, bytes):
|
||||
return val
|
||||
else:
|
||||
return strtobytes(val)
|
||||
|
||||
def cmdbuilder(name, *args, **kwargs):
|
||||
"""
|
||||
A helper for building the command arguments
|
||||
|
||||
args are the positional arguments
|
||||
|
||||
kwargs are the options
|
||||
keys that are single lettered are prepended with '-', others with '--',
|
||||
underscores are replaced with dashes
|
||||
|
||||
keys with False boolean values are ignored, lists add the key multiple times
|
||||
|
||||
None arguments are skipped
|
||||
|
||||
>>> cmdbuilder(b('cmd'), a=True, b=False, c=None) == [b('cmd'), b('-a')]
|
||||
True
|
||||
>>> cmdbuilder(b('cmd'), long=True) == [b('cmd'), b('--long')]
|
||||
True
|
||||
>>> cmdbuilder(b('cmd'), str=b('s')) == [b('cmd'), b('--str'), b('s')]
|
||||
True
|
||||
>>> cmdbuilder(b('cmd'), d_ash=True) == [b('cmd'), b('--d-ash')]
|
||||
True
|
||||
>>> cmdbuilder(b('cmd'), _=True) == [b('cmd'), b('-')]
|
||||
True
|
||||
>>> expect = [b('cmd'), b('--list'), b('1'), b('--list'), b('2')]
|
||||
>>> cmdbuilder(b('cmd'), list=[1, 2]) == expect
|
||||
True
|
||||
>>> cmdbuilder(b('cmd'), None) == [b('cmd')]
|
||||
True
|
||||
"""
|
||||
cmd = [name]
|
||||
for arg, val in kwargs.items():
|
||||
if val is None:
|
||||
continue
|
||||
|
||||
arg = arg.encode('latin-1').replace(b('_'), b('-'))
|
||||
if arg != b('-'):
|
||||
if len(arg) == 1:
|
||||
arg = b('-') + arg
|
||||
else:
|
||||
arg = b('--') + arg
|
||||
if isinstance(val, bool):
|
||||
if val:
|
||||
cmd.append(arg)
|
||||
elif isinstance(val, list):
|
||||
for v in val:
|
||||
cmd.append(arg)
|
||||
cmd.append(_cmdval(v))
|
||||
else:
|
||||
cmd.append(arg)
|
||||
cmd.append(_cmdval(val))
|
||||
|
||||
for a in args:
|
||||
if a is not None:
|
||||
cmd.append(a)
|
||||
|
||||
return cmd
|
||||
|
||||
class reterrorhandler(object):
|
||||
"""This class is meant to be used with rawcommand() error handler
|
||||
argument. It remembers the return value the command returned if
|
||||
it's one of allowed values, which is only 1 if none are given.
|
||||
Otherwise it raises a CommandError.
|
||||
|
||||
>>> e = reterrorhandler('')
|
||||
>>> bool(e)
|
||||
True
|
||||
>>> e(1, 'a', '')
|
||||
'a'
|
||||
>>> bool(e)
|
||||
False
|
||||
|
||||
"""
|
||||
def __init__(self, args, allowed=None):
|
||||
self.args = args
|
||||
self.ret = 0
|
||||
if allowed is None:
|
||||
self.allowed = [1]
|
||||
else:
|
||||
self.allowed = allowed
|
||||
|
||||
def __call__(self, ret, out, err):
|
||||
self.ret = ret
|
||||
if ret not in self.allowed:
|
||||
raise error.CommandError(self.args, ret, out, err)
|
||||
return out
|
||||
|
||||
def __nonzero__(self):
|
||||
""" Returns True if the return code was 0, False otherwise """
|
||||
return self.ret == 0
|
||||
|
||||
def __bool__(self):
|
||||
return self.__nonzero__()
|
||||
|
||||
class propertycache(object):
|
||||
"""
|
||||
Decorator that remembers the return value of a function call.
|
||||
|
||||
>>> execcount = 0
|
||||
>>> class obj(object):
|
||||
... def func(self):
|
||||
... global execcount
|
||||
... execcount += 1
|
||||
... return []
|
||||
... func = propertycache(func)
|
||||
>>> o = obj()
|
||||
>>> o.func
|
||||
[]
|
||||
>>> execcount
|
||||
1
|
||||
>>> o.func
|
||||
[]
|
||||
>>> execcount
|
||||
1
|
||||
"""
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.name = func.__name__
|
||||
def __get__(self, obj, type=None):
|
||||
result = self.func(obj)
|
||||
setattr(obj, self.name, result)
|
||||
return result
|
||||
|
||||
close_fds = os.name == 'posix'
|
||||
|
||||
startupinfo = None
|
||||
if os.name == 'nt':
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
||||
def popen(args, env=None):
|
||||
environ = None
|
||||
if env:
|
||||
environ = dict(os.environ)
|
||||
environ.update(env)
|
||||
|
||||
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=close_fds,
|
||||
startupinfo=startupinfo, env=environ)
|
|
@ -0,0 +1,54 @@
|
|||
import os, time
|
||||
from distutils.core import setup
|
||||
|
||||
# query Mercurial for version number, or pull from PKG-INFO
|
||||
version = 'unknown'
|
||||
if os.path.isdir('.hg'):
|
||||
cmd = "hg id -i -t"
|
||||
l = os.popen(cmd).read().split()
|
||||
while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
|
||||
l.pop()
|
||||
if len(l) > 1: # tag found
|
||||
version = l[-1]
|
||||
if l[0].endswith('+'): # propagate the dirty status to the tag
|
||||
version += '+'
|
||||
elif len(l) == 1: # no tag found
|
||||
cmd = 'hg parents --template "{latesttag}+{latesttagdistance}-"'
|
||||
version = os.popen(cmd).read() + l[0]
|
||||
if version.endswith('+'):
|
||||
version += time.strftime('%Y%m%d')
|
||||
elif os.path.exists('.hg_archival.txt'):
|
||||
kw = dict([[t.strip() for t in l.split(':', 1)]
|
||||
for l in open('.hg_archival.txt')])
|
||||
if 'tag' in kw:
|
||||
version = kw['tag']
|
||||
elif 'latesttag' in kw:
|
||||
version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
|
||||
else:
|
||||
version = kw.get('node', '')[:12]
|
||||
elif os.path.exists('PKG-INFO'):
|
||||
kw = dict([[t.strip() for t in l.split(':', 1)]
|
||||
for l in open('PKG-INFO') if ':' in l])
|
||||
version = kw.get('Version', version)
|
||||
|
||||
setup(
|
||||
name='python-hglib',
|
||||
version=version,
|
||||
author='Idan Kamara',
|
||||
author_email='idankk86@gmail.com',
|
||||
url='http://selenic.com/repo/python-hglib',
|
||||
description='Mercurial Python library',
|
||||
long_description=open(os.path.join(os.path.dirname(__file__),
|
||||
'README')).read(),
|
||||
classifiers=[
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.4',
|
||||
'Programming Language :: Python :: 2.5',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
|
||||
],
|
||||
license='MIT',
|
||||
packages=['hglib'])
|
Загрузка…
Ссылка в новой задаче