Merge branch 'release-1.0.0'

testing/mmn/mktime_takes_localtime_not_gmtime
Isis Lovecruft 2013-05-27 14:39:13 +00:00
commit 56a0928a4f
No known key found for this signature in database
GPG Key ID: A3ADB67A2CDB8B35
38 changed files with 8614 additions and 2394 deletions

1
.gitattributes vendored 100644
View File

@ -0,0 +1 @@
src/_version.py export-subst

25
.gitignore vendored
View File

@ -35,11 +35,13 @@ nosetests.xml
.pydevproject
# Temp files
.\#*
\#*\#
*~
# tags
TAGS
tags
# notes
*.org
@ -47,3 +49,26 @@ TAGS
# Ignore log files and directories from tests:
keys/*
*.log
# Ignore distutils record of installed files:
installed-files.txt
# Ignore PyCharm files:
.idea/*
# and git-tickets and tickets:
.tickets/*
tickets/*
# Ignore virtualenv folders, if they are here:
include/*
local/*
# Ignore gpg binary symlinks:
gpg
# Ignore keys generated during tests:
*generated-keys*
*.pubring
*.secring
*random_seed*

View File

@ -1,12 +0,0 @@
This file is part of python-gnupg, a Python wrapper around GnuPG.
Copyright (C) 2013 Isis Lovecruft
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

View File

@ -1,18 +1,65 @@
clean:
rm -f \#*\#
rm -f ./*.pyc
rm -f ./*.pyo
ctags:
ctags -R *.py
cleantest: clean
mkdir -p keys
etags:
find . -name "*.py" -print | xargs etags
cleanup-src:
cd src && \
rm -f \#*\# && \
rm -f ./*.pyc && \
rm -f ./*.pyo
cleanup-tests:
cd tests && \
rm -f \#*\# && \
rm -f ./*.pyc && \
rm -f ./*.pyo
mkdir -p tests/tmp
mkdir -p tests/logs
touch tests/placeholder.log
mv tests/*.log tests/logs/
rm tests/logs/placeholder.log
touch placeholder.log
rm -rf keys
rm *.log
rm tests/random_seed
test: cleantest
python test_gnupg.py basic
cleanup-tests-all: cleanup-tests
rm -rf tests/tmp
cleanup-build:
mkdir buildnot
rm -rf build*
test: cleanup-src cleanup-tests
which gpg
gpg --version
which gpg2
gpg2 --version
which gpg-agent
which pinentry
which python
python --version
which pip
pip --version
pip list
python tests/test_gnupg.py parsers basic encodings genkey sign listkeys crypt keyrings import
install:
python setup.py install
python setup.py install --record installed-files.txt
uninstall:
touch installed-files.txt
cat installed-files.txt | sudo xargs rm -rf
cleandocs:
sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs src/ tests/
docs:
cd docs
make clean
make html
venv:
-source /usr/shared/python/ns/virtualenvwrapper.sh && mkvirtualenv -a "$PWD" --no-site-packages --unzip-setuptools --distribute python-gnupg

View File

@ -5,24 +5,25 @@ Summary: A wrapper for the Gnu Privacy Guard (GPG or GnuPG)
Home-page: https://www.github.com/isislovecruft/python-gnupg
Author: Isis Lovecruft
Author-email: isis@leap.se
License: Copyright (C) 2013 by Isis Lovecruft. All Rights Reserved. See LICENSE for license. See COPYLEFT for "copyright".
Download-URL: https://github.com/isislovecruft/python-gnupg.git
Description: This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.4 or greater.
Platform: No particular restrictions
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 2.4
Classifier: Programming Language :: Python :: 2.5
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.0
Classifier: Programming Language :: Python :: 3.1
Classifier: Programming Language :: Python :: 3.2
Classifier: Operating System :: OS Independent
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Download-URL: https://github.com/isislovecruft/python-gnupg/archives/develop.zip
Description:: This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.6 or greater.
Classifier:: Development Status :: 3 - Alpha
License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows :: Windows 7
Operating System :: Microsoft :: Windows :: Windows XP
Operating System :: POSIX :: BSD
Operating System :: POSIX :: Linux
Classifier:: Intended Audience :: Developers
Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier:: Programming Language :: Python
Classifier:: Programming Language :: Python :: 2
Classifier:: Programming Language :: Python :: 3
Classifier:: Programming Language :: Python :: 2.6
Classifier:: Programming Language :: Python :: 2.7
Classifier:: Programming Language :: Python :: 3.0
Classifier:: Programming Language :: Python :: 3.1
Classifier:: Programming Language :: Python :: 3.2
Classifier:: Topic :: Security :: Cryptography
Classifier:: Topic :: Software Development :: Libraries :: Python Modules
Classifier:: Topic :: Utilities

12
README
View File

@ -1,12 +0,0 @@
python-gnupg
============
Fork of python-gnupg-0.3.2, patched to remove Popen([...], shell=True).
Installation
------------
To install this package from a source distribution, do the following.
1. Extract all the files in the distribution archive to some directory on your system.
2. In that directory, run "python setup.py install".
3. Optionally, run "python test_gnupg.py" to ensure that the package is working as expected.

63
README.md 100644
View File

@ -0,0 +1,63 @@
# python-gnupg #
================
Fork of python-gnupg-0.3.2, patched to remove ```Popen([...], shell=True)```.
### Installation ###
#### From this git repository ####
To install this package from this git repository, do:
```
git clone https://github.com/isislovecruft/python-gnupg.git
cd python-gnupg
make install
make test
```
Optionally to build the documentation after installation, do:
```
make docs
```
To get started using python-gnupg's API, see the [online documentation](https://python-gnupg.readthedocs.org/en/latest/),
and import the module like so:
```
>>> import gnupg
```
The primary interface class you'll likely want to interact with is
[```gnupg.GPG```](https://python-gnupg.readthedocs.org/en/latest/gnupg.html#gpg):
```
>>> gpg = gnupg.GPG(gpgbinary='/usr/bin/gpg',
... gpghome='./keys',
... pubring='pubring.gpg',
... secring='secring.gpg')
>>> batch_key_input = gpg.gen_key_input()
>>> print batch_key_input
Key-Type: RSA
Name-Email: isis@wintermute
Name-Comment: Generated by gnupg.py
Key-Length: 4096
Name-Real: Autogenerated Key
%pubring /home/isis/code/python-gnupg/keys/pubring.gpg
%secring /home/isis/code/python-gnupg/keys/secring.gpg
%commit
>>> key = gpg.gen_key(batch_key_input)
>>> print key.fingerprint
245D8FA30F543B742053949F553C0E154F2E7A98
```
#### From PyPI ####
Hold your horses, boy. I haven't finished development, so the packages on
[PyPI](https://pypi.python.org) are still the old versions belonging to the
other authors.
### Bug Reports & Feature Requests ###
Our bugtracker is [here](https://leap.se/code/projects/eip_server/issue/new).
Please use that for bug reports and feature requests instead of github's
tracker. We're using github for code commenting and review between
collaborators.

60
TODO 100644
View File

@ -0,0 +1,60 @@
-*- mode: org -*-
* Keyring separation :keyseparation:
** TODO in GPG.gen_key() :keyseparation:gen_key:
It would be nice to have an option for gen_key() [[gnupg.py:927]] to
automatically switch before key generation to a new tempfile.mkdtemp()
directory, with a new keyring and secring, and then to rename either the
directory or the keyrings with the long keyid of the key which was freshly
generated.
* I/O :io:
** TODO in GPG.__make_args() :io:makeargs:
It would be nice to make the file descriptors for communication with the GnuPG
process configurable, and not the default, hard-coded 0=stdin 1=stdout
2=stderr.
* Key editing :editkey:
** TODO add '--edit-key' feature :editkey:
see :compatibility:gen__key_input:
* Compatibility between GnuPG versions :compatibility:
** TODO GnuPG>=2.1.0 won't allow key generation with preset passphrase
*** TODO in GPG.gen__key_input() :compatibility:gen_key_input:
In the docstring of GPG.gen__key_input() [[gnupg.py:1068]], for the parameter
'passphrase', it is explained that:
:param str passphrase: The passphrase for the new key. The default is
to not use any passphrase. Note that
GnuPG>=2.1.x will not allow you to specify a
passphrase for batch key generation -- GnuPG
will ignore the ``passphrase`` parameter, stop,
and ask the user for the new passphrase.
However, we can put the command '%no-protection'
into the batch key generation file to allow a
passwordless key to be created, which can then
have its passphrase set later with '--edit-key'.
If we add a GnuPG version detection feature (the version string is already
obtained in GPG.___init___() [[gnupg.py:407]]), then we can automatically chain
GPG.gen__key_input() to another new feature for '--edit-key'. This chaining
would likely need to happen here [[gnupg.py:1146]].
*** TODO add '--edit-key' feature :editkey:
This would be necessary for adding a passphrase to the key after passwordless
generation in GnuPG>=2.1.0.
* Code cleanup :cleanup:
** TODO in parsers.__sanitise() :cleanup:sanitise:
Ughh...this is the ugliest code I think I've ever written. It works, but I
worry that it is fragile, not to mention *I* have trouble reading it, and I
fucking wrote the damn thing. There's probably not much that could be done to
make it more Pythonic, because type checks and input validation are pretty much
intrinsically non-Pythonic. But did i mention that it's ugly? I'm sure these
functions would be pretty glad to get a shower, shave, and haircut.
** TODO in parsers.__is_allowed() :cleanup:is_allowed:
There is a lot of madness dealing with stupid things like hyphens
vs. underscores, and lists of options vs. strings. This can *definitely* be
cleaned up.

1225
docs/DETAILS 100644

File diff suppressed because it is too large Load Diff

153
docs/Makefile 100644
View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-gnupg.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-gnupg.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/python-gnupg"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-gnupg"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

Binary file not shown.

Binary file not shown.

386
docs/_static/haiku.css vendored 100644
View File

@ -0,0 +1,386 @@
/* custom stuff I put in FIXME where is it "supposed" to go? */
div.admonition-todo
{
border: 1px solid red;
background-color: #Fdd;
}
div.admonition-todo p.admonition-title
{
margin: 0;
color: red;
text-transform: lowercase;
}
p.admonition-title
{
font-size: 120%;
font-weight: bold;
}
dl.class>dt, dl.interface>dt, dl.function>dt, dl.staticmethod>dt
{
font-size: 150%;
background-color:#ddd;
}
dl.method>dt
{
background-color: #eee;
border-bottom: 2px solid #ddd;
}
dl.method:hover
{
background-color:#ffd;
}
/** end custom */
html {
margin: 0px;
padding: 0px;
background: #FFF url(bg-page.png) top left repeat-x;
}
body {
line-height: 1.5;
margin: auto;
padding: 0px;
font-family: "DejaVu Sans", Arial, Helvetica, sans-serif;
min-width: 59em;
max-width: 70em;
color: #333333;
}
div.footer {
padding: 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.5px;
}
/* link colors and text decoration */
a:link {
font-weight: bold;
text-decoration: none;
color: #dc3c01;
}
a:visited {
font-weight: bold;
text-decoration: none;
color: #892601;
}
a:hover, a:active {
text-decoration: underline;
color: #ff4500;
}
/* Some headers act as anchors, don't give them a hover effect */
h1 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h2 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h3 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h4 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
a.headerlink {
color: #a7ce38;
padding-left: 5px;
}
a.headerlink:hover {
color: #a7ce38;
}
/* basic text elements */
div.content {
margin-top: 20px;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 50px;
font-size: 0.9em;
}
/* heading and navigation */
div.header {
position: relative;
left: 0px;
top: 0px;
height: 85px;
/* background: #eeeeee; */
padding: 0 40px;
}
div.header h1 {
font-size: 1.6em;
font-weight: normal;
letter-spacing: 1px;
color: #0c3762;
border: 0;
margin: 0;
padding-top: 15px;
}
div.header h1 a {
font-weight: normal;
color: #0c3762;
}
div.header h2 {
font-size: 1.3em;
font-weight: normal;
letter-spacing: 1px;
text-transform: uppercase;
color: #aaa;
border: 0;
margin-top: -3px;
padding: 0;
}
div.header img.rightlogo {
float: right;
}
div.title {
font-size: 1.3em;
font-weight: bold;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-bottom: 25px;
}
div.topnav {
/* background: #e0e0e0; */
}
div.topnav p {
margin-top: 0;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 0px;
text-align: right;
font-size: 0.8em;
}
div.bottomnav {
background: #eeeeee;
}
div.bottomnav p {
margin-right: 40px;
text-align: right;
font-size: 0.8em;
}
a.uplink {
font-weight: normal;
}
/* contents box */
table.index {
margin: 0px 0px 30px 30px;
padding: 1px;
border-width: 1px;
border-style: dotted;
border-color: #e0e0e0;
}
table.index tr.heading {
background-color: #e0e0e0;
text-align: center;
font-weight: bold;
font-size: 1.1em;
}
table.index tr.index {
background-color: #eeeeee;
}
table.index td {
padding: 5px 20px;
}
table.index a:link, table.index a:visited {
font-weight: normal;
text-decoration: none;
color: #dc3c01;
}
table.index a:hover, table.index a:active {
text-decoration: underline;
color: #ff4500;
}
/* Haiku User Guide styles and layout */
/* Rounded corner boxes */
/* Common declarations */
div.admonition {
-webkit-border-radius: 10px;
-khtml-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border-style: dotted;
border-width: thin;
border-color: #dcdcdc;
padding: 10px 15px 10px 15px;
margin-bottom: 15px;
margin-top: 15px;
}
div.note {
padding: 10px 15px 10px 80px;
background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat;
min-height: 42px;
}
div.warning {
padding: 10px 15px 10px 80px;
background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat;
min-height: 42px;
}
div.seealso {
background: #e4ffde;
}
/* More layout and styles */
h1 {
font-size: 1.3em;
font-weight: bold;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
}
h2 {
font-size: 1.2em;
font-weight: normal;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
}
h3 {
font-size: 1.1em;
font-weight: normal;
color: #0c3762;
margin-top: 30px;
}
h4 {
font-size: 1.0em;
font-weight: normal;
color: #0c3762;
margin-top: 30px;
}
p {
text-align: justify;
}
p.last {
margin-bottom: 0;
}
ol {
padding-left: 20px;
}
ul {
padding-left: 5px;
margin-top: 3px;
}
li {
line-height: 1.3;
}
div.content ul > li {
-moz-background-clip:border;
-moz-background-inline-policy:continuous;
-moz-background-origin:padding;
background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em;
list-style-image: none;
list-style-type: none;
padding: 0 0 0 1.666em;
margin-bottom: 3px;
}
td {
vertical-align: top;
}
tt {
background-color: #e2e2e2;
font-size: 1.0em;
font-family: monospace;
}
pre {
border-color: #0c3762;
border-style: dotted;
border-width: thin;
margin: 0 0 12px 0;
padding: 0.8em;
background-color: #f0f0f0;
}
hr {
border-top: 1px solid #ccc;
border-bottom: 0;
border-right: 0;
border-left: 0;
margin-bottom: 10px;
margin-top: 20px;
}
/* printer only pretty stuff */
@media print {
.noprint {
display: none;
}
/* for acronyms we want their definitions inlined at print time */
acronym[title]:after {
font-size: small;
content: " (" attr(title) ")";
font-style: italic;
}
/* and not have mozilla dotted underline */
acronym {
border: none;
}
div.topnav, div.bottomnav, div.header, table.index {
display: none;
}
div.content {
margin: 0px;
padding: 0px;
}
html {
background: #FFF;
}
}
.viewcode-back {
font-family: "DejaVu Sans", Arial, Helvetica, sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
margin: -1px -12px;
padding: 0 12px;
}

306
docs/conf.py 100644
View File

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
#
# python-gnupg documentation build configuration file, created by
# sphinx-quickstart on Fri Apr 5 22:38:47 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('./../gnupg'))
sys.path.insert(0, os.path.abspath('.'))
# -- Autodoc settings ----------------------------------------------------------
## trying to set this somewhere...
autodoc_member_order = 'bysource'
autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members']
autoclass_content = 'both'
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'python-gnupg'
copyright = u'2013, Isis Agora Lovecruft'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
from gnupg import __version__
version = __version__
# The full version, including alpha/beta/rc tags.
release = __version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'scrolls'
html_theme = 'traditional'
html_theme = 'nature'
html_theme = 'pyramid'
html_theme = 'agogo'
html_theme = 'haiku'
html_theme_options = {
# 'stickysidebar': 'true',
# 'rightsidebar':'true',
'nosidebar': 'false',
# 'full_logo': 'false'
'sidebarwidth': '300'
}
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
html_show_copyright = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-gnupgdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'python-gnupg.tex', u'python-gnupg Documentation',
u'Isis Agora Lovecruft', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'python-gnupg', u'python-gnupg Documentation',
[u'Isis Agora Lovecruft'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'python-gnupg', u'python-gnupg Documentation',
u'Isis Agora Lovecruft', 'python-gnupg', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = u'python-gnupg'
epub_author = u'Isis Agora Lovecruft'
epub_publisher = u'Isis Agora Lovecruft'
epub_copyright = u'2013, Isis Agora Lovecruft'
# The language of the text. It defaults to the language option
# or en if the language is not set.
#epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
#epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#epub_identifier = ''
# A unique identification for the text.
#epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
#epub_cover = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_post_files = []
# A list of files that should not be packed into the epub file.
#epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
#epub_tocdepth = 3
# Allow duplicate toc entries.
#epub_tocdup = True

7
docs/gnupg.rst 100644
View File

@ -0,0 +1,7 @@
gnupg Module
============
.. automodule:: gnupg
:members:
:undoc-members:
:show-inheritance:

24
docs/index.rst 100644
View File

@ -0,0 +1,24 @@
.. python-gnupg documentation master file, created by
sphinx-quickstart on Fri Apr 5 22:38:47 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
python-gnupg documentation
==========================
Contents:
.. toctree::
:maxdepth: 4
gnupg
parsers
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

190
docs/make.bat 100644
View File

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-gnupg.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-gnupg.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

7
docs/parsers.rst 100644
View File

@ -0,0 +1,7 @@
parsers Module
==============
.. automodule:: parsers
:members:
:undoc-members:
:show-inheritance:

1760
gnupg.py

File diff suppressed because it is too large Load Diff

2
requirements.txt 100644
View File

@ -0,0 +1,2 @@
Sphinx>=1.1
psutil>=0.5.1

View File

@ -1,37 +1,48 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
from distutils.core import setup
from gnupg import __version__ as version
import versioneer
versioneer.versionfile_source = 'src/_version.py'
versioneer.versionfile_build = 'gnupg/_version.py'
versioneer.tag_prefix = ''
versioneer.parentdir_prefix = 'python-gnupg-'
__author__ = "Isis Agora Lovecruft"
__contact__ = 'isis@leap.se'
setup(name = "python-gnupg",
description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)",
long_description = "This module allows easy access to GnuPG's key \
description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)",
long_description = "This module allows easy access to GnuPG's key \
management, encryption and signature functionality from Python programs. \
It is intended for use with Python 2.4 or greater.",
license="""Copyright (C) 2008-2012 by Vinay Sajip. All Rights Reserved. See LICENSE for license.""",
version=version,
author="Vinay Sajip",
author_email="vinay_sajip@red-dove.com",
maintainer="Vinay Sajip",
maintainer_email="vinay_sajip@red-dove.com",
url="http://packages.python.org/python-gnupg/index.html",
py_modules=["gnupg"],
platforms="No particular restrictions",
download_url="http://python-gnupg.googlecode.com/files/python-gnupg-%s.tar.gz" % version,
classifiers=[
'Development Status :: 5 - Production/Stable',
"Intended Audience :: Developers",
'License :: OSI Approved :: BSD License',
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.4",
"Programming Language :: Python :: 2.5",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.0",
"Programming Language :: Python :: 3.1",
"Programming Language :: Python :: 3.2",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules"
]
)
It is intended for use with Python 2.6 or greater.",
license="""Copyright © 2013 Isis Lovecruft, et.al. see LICENSE file.""",
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
author=__author__,
author_email=__contact__,
maintainer=__author__,
maintainer_email=__contact__,
url="https://github.com/isislovecruft/python-gnupg",
package_dir={'gnupg': 'src'},
packages=['gnupg'],
platforms="Linux, BSD, OSX, Windows",
download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip",
classifiers=[
'Development Status :: 4 - Alpha',
"Intended Audience :: Developers",
'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.0",
"Programming Language :: Python :: 3.1",
"Programming Language :: Python :: 3.2",
"Topic :: Software Development :: Libraries :: Python Modules",
'Classifier:: Topic :: Security :: Cryptography',
'Classifier:: Topic :: Software Development :: Libraries :: Python Modules',
'Classifier:: Topic :: Utilities',]
)

21
src/__init__.py 100644
View File

@ -0,0 +1,21 @@
import gnupg
import copyleft
from gnupg import GPG
from ._version import get_versions
__version__ = get_versions()['version']
gnupg.__version__ = __version__
gnupg.__author__ = 'Isis Agora Lovecruft'
gnupg.__contact__ = 'isis@leap.se'
gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg'
gnupg.__license__ = copyleft.disclaimer
__all__ = ["GPG"]
del gnupg
del copyleft
del get_versions
del _version

172
src/_ansistrm.py 100644
View File

@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
#
# This file is part of python-gnupg, a Python wrapper aroung GnuPG, and it was
# taken from https://gist.github.com/vsajip/758430 on the 14th of May, 2013. It
# has also been included in the 'logutils' Python module, see
# https://code.google.com/p/logutils/ .
#
# The original copyright and license text are as follows:
# |
# | Copyright (C) 2010-2012 Vinay Sajip. All rights reserved.
# | Licensed under the new BSD license.
# |
#
# This file is part of python-gnupg, a Python wrapper around GnuPG.
# Copyright © 2013 Isis Lovecruft, Andrej B.
# © 2008-2012 Vinay Sajip
# © 2005 Steve Traugott
# © 2004 A.M. Kuchling
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
import ctypes
import logging
import os
class ColorizingStreamHandler(logging.StreamHandler):
# color names to indices
color_map = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'white': 7,
}
#levels to (background, foreground, bold/intense)
if os.name == 'nt':
level_map = {
logging.DEBUG: (None, 'blue', True),
logging.INFO: (None, 'green', False),
logging.WARNING: (None, 'yellow', True),
logging.ERROR: (None, 'red', True),
logging.CRITICAL: ('red', 'white', True),
}
else:
level_map = {
logging.DEBUG: (None, 'blue', False),
logging.INFO: (None, 'green', False),
logging.WARNING: (None, 'yellow', False),
logging.ERROR: (None, 'red', False),
logging.CRITICAL: ('red', 'white', True),
}
csi = '\x1b['
reset = '\x1b[0m'
@property
def is_tty(self):
isatty = getattr(self.stream, 'isatty', None)
return isatty and isatty()
def emit(self, record):
try:
message = self.format(record)
stream = self.stream
if not self.is_tty:
stream.write(message)
else:
self.output_colorized(message)
stream.write(getattr(self, 'terminator', '\n'))
self.flush()
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
if os.name != 'nt':
def output_colorized(self, message):
self.stream.write(message)
else:
import re
ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m')
nt_color_map = {
0: 0x00, # black
1: 0x04, # red
2: 0x02, # green
3: 0x06, # yellow
4: 0x01, # blue
5: 0x05, # magenta
6: 0x03, # cyan
7: 0x07, # white
}
def output_colorized(self, message):
parts = self.ansi_esc.split(message)
write = self.stream.write
h = None
fd = getattr(self.stream, 'fileno', None)
if fd is not None:
fd = fd()
if fd in (1, 2): # stdout or stderr
h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
while parts:
text = parts.pop(0)
if text:
write(text)
if parts:
params = parts.pop(0)
if h is not None:
params = [int(p) for p in params.split(';')]
color = 0
for p in params:
if 40 <= p <= 47:
color |= self.nt_color_map[p - 40] << 4
elif 30 <= p <= 37:
color |= self.nt_color_map[p - 30]
elif p == 1:
color |= 0x08 # foreground intensity on
elif p == 0: # reset to default color
color = 0x07
else:
pass # error condition ignored
ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
def colorize(self, message, record):
if record.levelno in self.level_map:
bg, fg, bold = self.level_map[record.levelno]
params = []
if bg in self.color_map:
params.append(str(self.color_map[bg] + 40))
if fg in self.color_map:
params.append(str(self.color_map[fg] + 30))
if bold:
params.append('1')
if params:
message = ''.join((self.csi, ';'.join(params),
'm', message, self.reset))
return message
def format(self, record):
message = logging.StreamHandler.format(self, record)
if self.is_tty:
# Don't colorize any traceback
parts = message.split('\n', 1)
parts[0] = self.colorize(parts[0], record)
message = '\n'.join(parts)
return message
def main():
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(ColorizingStreamHandler())
logging.debug('DEBUG')
logging.info('INFO')
logging.warning('WARNING')
logging.error('ERROR')
logging.critical('CRITICAL')
if __name__ == '__main__':
main()

97
src/_logger.py 100644
View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
#
# This file is part of python-gnupg, a Python wrapper around GnuPG.
# Copyright © 2013 Isis Lovecruft, Andrej B.
# © 2008-2012 Vinay Sajip
# © 2005 Steve Traugott
# © 2004 A.M. Kuchling
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
'''log.py
----------
Logging module for python-gnupg.
'''
from __future__ import print_function
from datetime import datetime
from functools import wraps
import logging
import sys
import os
import _ansistrm
try:
from logging import NullHandler
except:
class NullHandler(logging.Handler):
def handle(self, record):
pass
GNUPG_STATUS_LEVEL = 9
def status(self, message, *args, **kwargs):
"""LogRecord for GnuPG internal status messages."""
if self.isEnabledFor(GNUPG_STATUS_LEVEL):
self._log(GNUPG_STATUS_LEVEL, message, args, **kwargs)
@wraps(logging.Logger)
def create_logger(level=logging.NOTSET):
"""Create a logger for python-gnupg at a specific message level.
:type level: int or str
:param level: A string or an integer for the lowest level to log.
Available levels:
int str description
0 NOTSET Disable all logging.
9 GNUPG Log GnuPG's internal status messages.
10 DEBUG Log module level debuging messages.
20 INFO Normal user-level messages.
30 WARN Warning messages.
40 ERROR Error messages and tracebacks.
50 CRITICAL Unhandled exceptions and tracebacks.
"""
_test = os.path.join(os.getcwd(), 'tests')
_now = datetime.now().strftime("%Y-%m-%d_%H%M%S")
_fn = os.path.join(_test, "%s_test_gnupg.log" % _now)
_fmt = "%(relativeCreated)-4d L%(lineno)-4d:%(funcName)-18.18s %(levelname)-7.7s %(message)s"
## Add the GNUPG_STATUS_LEVEL LogRecord to all Loggers in the module:
logging.addLevelName(GNUPG_STATUS_LEVEL, "GNUPG")
logging.Logger.status = status
if level > logging.NOTSET:
logging.basicConfig(level=level, filename=_fn,
filemode="a", format=_fmt)
logging.captureWarnings(True)
logging.logThreads = True
colouriser = _ansistrm.ColorizingStreamHandler
colouriser.level_map[9] = (None, 'blue', False)
colouriser.level_map[10] = (None, 'cyan', False)
handler = colouriser(stream=sys.stderr)
handler.setLevel(level)
formatr = logging.Formatter(_fmt)
handler.setFormatter(formatr)
print("Starting the logger...")
else:
handler = NullHandler()
print("GnuPG logging disabled...")
log = logging.getLogger('gnupg')
log.addHandler(handler)
log.setLevel(level)
log.info("Log opened: %s UTC" % datetime.ctime(datetime.utcnow()))
return log

1186
src/_parsers.py 100644

File diff suppressed because it is too large Load Diff

470
src/_util.py 100644
View File

@ -0,0 +1,470 @@
# -*- coding: utf-8 -*-
#
# This file is part of python-gnupg, a Python wrapper around GnuPG.
# Copyright © 2013 Isis Lovecruft, Andrej B.
# © 2008-2012 Vinay Sajip
# © 2005 Steve Traugott
# © 2004 A.M. Kuchling
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
'''
util.py
----------
Extra utilities for python-gnupg.
'''
from datetime import datetime
from socket import gethostname
import codecs
import encodings
import os
import time
import threading
import random
import string
import sys
import _logger
try:
from io import StringIO
from io import BytesIO
except ImportError:
from cStringIO import StringIO
try:
unicode
_py3k = False
try:
isinstance(__name__, basestring)
except NameError:
msg = "Sorry, python-gnupg requires a Python version with proper"
msg += " unicode support. Please upgrade to Python>=2.3."
raise SystemExit(msg)
except NameError:
_py3k = True
## Directory shortcuts:
_here = os.getcwd()
_test = os.path.join(os.path.join(_here, 'tests'), 'tmp') ## ./tests/tmp
_user = os.environ.get('HOME') ## $HOME
_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg
_conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg')
## $HOME/.config/python-gnupg
## Logger is disabled by default
log = _logger.create_logger(0)
def find_encodings(enc=None, system=False):
"""Find functions for encoding translations for a specific codec.
:param str enc: The codec to find translation functions for. It will be
normalized by converting to lowercase, excluding
everything which is not ascii, and hyphens will be
converted to underscores.
:param bool system: If True, find encodings based on the system's stdin
encoding, otherwise assume utf-8.
:raises: :exc:LookupError if the normalized codec, ``enc``, cannot be
found in Python's encoding translation map.
"""
if not enc:
enc = 'utf-8'
if system:
if getattr(sys.stdin, 'encoding', None) is None:
enc = sys.stdin.encoding
log.debug("Obtained encoding from stdin: %s" % enc)
else:
enc = 'ascii'
## have to have lowercase to work, see
## http://docs.python.org/dev/library/codecs.html#standard-encodings
enc = enc.lower()
codec_alias = encodings.normalize_encoding(enc)
codecs.register(encodings.search_function)
coder = codecs.lookup(codec_alias)
return coder
def _copy_data(instream, outstream):
"""Copy data from one stream to another.
:type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file
:param instream: A byte stream or open file to read from.
:param file outstream: The file descriptor of a tmpfile to write to.
"""
sent = 0
#try:
# #assert (util._is_stream(instream)
# # or isinstance(instream, file)), "instream not stream or file"
# assert isinstance(outstream, file), "outstream is not a file"
#except AssertionError as ae:
# log.exception(ae)
# return
coder = find_encodings()
while True:
if ((_py3k and isinstance(instream, str)) or
(not _py3k and isinstance(instream, basestring))):
data = instream[:1024]
instream = instream[1024:]
else:
data = instream.read(1024)
if len(data) == 0:
break
sent += len(data)
log.debug("Sending chunk %d bytes:\n%s"
% (sent, data))
try:
outstream.write(data)
except UnicodeError:
try:
outstream.write(coder.encode(data))
except IOError:
log.exception("Error sending data: Broken pipe")
break
except IOError:
# Can get 'broken pipe' errors even when all data was sent
log.exception('Error sending data: Broken pipe')
break
try:
outstream.close()
except IOError as ioe:
log.error("Unable to close outstream %s:\r\t%s" % (outstream, ioe))
else:
log.debug("Closed outstream: %d bytes sent." % sent)
def _create_if_necessary(directory):
"""Create the specified directory, if necessary.
:param str directory: The directory to use.
:rtype: bool
:returns: True if no errors occurred and the directory was created or
existed beforehand, False otherwise.
"""
if not os.path.isabs(directory):
log.debug("Got non-absolute path: %s" % directory)
directory = os.path.abspath(directory)
if not os.path.isdir(directory):
log.info("Creating directory: %s" % directory)
try:
os.makedirs(directory, 0x1C0)
except OSError as ose:
log.error(ose, exc_info=1)
return False
else:
log.debug("Created directory.")
return True
def create_uid_email(username=None, hostname=None):
"""Create an email address suitable for a UID on a GnuPG key.
:param str username: The username portion of an email address. If None,
defaults to the username of the running Python
process.
:param str hostname: The FQDN portion of an email address. If None, the
hostname is obtained from gethostname(2).
:rtype: str
:returns: A string formatted as <username>@<hostname>.
"""
if hostname:
hostname = hostname.replace(' ', '_')
if not username:
try: username = os.environ['LOGNAME']
except KeyError: username = os.environ['USERNAME']
if not hostname: hostname = gethostname()
uid = "%s@%s" % (username.replace(' ', '_'), hostname)
else:
username = username.replace(' ', '_')
if (not hostname) and (username.find('@') == 0):
uid = "%s@%s" % (username, gethostname())
elif hostname:
uid = "%s@%s" % (username, hostname)
else:
uid = username
return uid
def _find_binary(binary=None):
"""Find the absolute path to the GnuPG binary.
Also run checks that the binary is not a symlink, and check that
our process real uid has exec permissions.
:param str binary: The path to the GnuPG binary.
:raises: :exc:RuntimeError if it appears that GnuPG is not installed.
:rtype: str
:returns: The absolute path to the GnuPG binary to use, if no exceptions
occur.
"""
gpg_binary = None
if binary is not None:
if not os.path.isabs(binary):
try: binary = _which(binary)[0]
except IndexError as ie:
log.error(ie.message)
if binary is None:
try: binary = _which('gpg')[0]
except IndexError: raise RuntimeError("GnuPG is not installed!")
try:
assert os.path.isabs(binary), "Path to gpg binary not absolute"
assert not os.path.islink(binary), "Path to gpg binary is symlink"
assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary"
except (AssertionError, AttributeError) as ae:
log.error(ae.message)
else:
return binary
def _has_readwrite(path):
"""
Determine if the real uid/gid of the executing user has read and write
permissions for a directory or a file.
:param str path: The path to the directory or file to check permissions
for.
:rtype: bool
:returns: True if real uid/gid has read+write permissions, False otherwise.
"""
return os.access(path, os.R_OK ^ os.W_OK)
def _is_file(input):
"""Check that the size of the thing which is supposed to be a filename has
size greater than zero, without following symbolic links or using
:func:os.path.isfile.
:param input: An object to check.
:rtype: bool
:returns: True if :param:input is file-like, False otherwise.
"""
try:
assert os.lstat(input).st_size > 0, "not a file: %s" % input
except (AssertionError, TypeError, IOError, OSError) as err:
log.error(err.message, exc_info=1)
return False
else:
return True
def _is_stream(input):
"""Check that the input is a byte stream.
:param input: An object provided for reading from or writing to.
:rtype: bool
:returns: True if :param:input is a stream, False if otherwise.
"""
return isinstance(input, BytesIO) or isinstance(input, StringIO)
def _is_list_or_tuple(instance):
"""Check that ``instance`` is a list or tuple.
:param instance: The object to type check.
:rtype: bool
:returns: True if ``instance`` is a list or tuple, False otherwise.
"""
return isinstance(instance, (list, tuple,))
def _make_binary_stream(s, encoding):
"""
xxx fill me in
"""
try:
if _py3k:
if isinstance(s, str):
s = s.encode(encoding)
else:
if type(s) is not str:
s = s.encode(encoding)
from io import BytesIO
rv = BytesIO(s)
except ImportError:
rv = StringIO(s)
return rv
def _make_passphrase(length=None, save=False, file=None):
"""Create a passphrase and write it to a file that only the user can read.
This is not very secure, and should not be relied upon for actual key
passphrases.
:param int length: The length in bytes of the string to generate.
:param file file: The file to save the generated passphrase in. If not
given, defaults to 'passphrase-<the real user id>-<seconds since
epoch>' in the top-level directory.
"""
if not length:
length = 40
passphrase = _make_random_string(length)
if save:
ruid, euid, suid = os.getresuid()
gid = os.getgid()
now = time.mktime(time.gmtime())
if not file:
filename = str('passphrase-%s-%s' % uid, now)
file = os.path.join(_repo, filename)
with open(file, 'a') as fh:
fh.write(passphrase)
fh.flush()
fh.close()
os.chmod(file, 0600)
os.chown(file, ruid, gid)
log.warn("Generated passphrase saved to %s" % file)
return passphrase
def _make_random_string(length):
"""Returns a random lowercase, uppercase, alphanumerical string.
:param int length: The length in bytes of the string to generate.
"""
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for x in range(length))
def _next_year():
"""Get the date of today plus one year.
:rtype: str
:returns: The date of this day next year, in the format '%Y-%m-%d'.
"""
now = datetime.now().__str__()
date = now.split(' ', 1)[0]
year, month, day = date.split('-', 2)
next_year = str(int(year)+1)
return '-'.join((next_year, month, day))
def _now():
"""Get a timestamp for right now, formatted according to ISO 8601."""
return datetime.isoformat(datetime.now())
def _threaded_copy_data(instream, outstream):
"""Copy data from one stream to another in a separate thread.
Wraps ``_copy_data()`` in a :class:`threading.Thread`.
:type instream: :class:`io.BytesIO` or :class:`io.StringIO`
:param instream: A byte stream to read from.
:param file outstream: The file descriptor of a tmpfile to write to.
"""
copy_thread = threading.Thread(target=_copy_data,
args=(instream, outstream))
copy_thread.setDaemon(True)
log.debug('%r, %r, %r', copy_thread, instream, outstream)
copy_thread.start()
return copy_thread
def _utc_epoch():
"""Get the seconds since epoch for UTC."""
return int(time.mktime(time.gmtime()))
def _which(executable, flags=os.X_OK):
"""Borrowed from Twisted's :mod:twisted.python.proutils .
Search PATH for executable files with the given name.
On newer versions of MS-Windows, the PATHEXT environment variable will be
set to the list of file extensions for files considered executable. This
will normally include things like ".EXE". This fuction will also find files
with the given name ending with any of these extensions.
On MS-Windows the only flag that has any meaning is os.F_OK. Any other
flags will be ignored.
Note: This function does not help us prevent an attacker who can already
manipulate the environment's PATH settings from placing malicious code
higher in the PATH. It also does happily follows links.
:param str name: The name for which to search.
:param int flags: Arguments to L{os.access}.
:rtype: list
:returns: A list of the full paths to files found, in the order in which
they were found.
"""
result = []
exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep))
path = os.environ.get('PATH', None)
if path is None:
return []
for p in os.environ.get('PATH', '').split(os.pathsep):
p = os.path.join(p, executable)
if os.access(p, flags):
result.append(p)
for e in exts:
pext = p + e
if os.access(pext, flags):
result.append(pext)
return result
def _write_passphrase(stream, passphrase, encoding):
"""Write the passphrase from memory to the GnuPG process' stdin.
:type stream: file, :class:BytesIO, or :class:StringIO
:param stream: The input file descriptor to write the password to.
:param str passphrase: The passphrase for the secret key material.
:param str encoding: The data encoding expected by GnuPG. Usually, this
is ``sys.getfilesystemencoding()``.
"""
passphrase = '%s\n' % passphrase
passphrase = passphrase.encode(encoding)
stream.write(passphrase)
log.debug("Wrote passphrase on stdin.")
class InheritableProperty(object):
"""Based on the emulation of PyProperty_Type() in Objects/descrobject.c"""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError, "unreadable attribute"
if self.fget.__name__ == '<lambda>' or not self.fget.__name__:
return self.fget(obj)
else:
return getattr(obj, self.fget.__name__)()
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError, "can't set attribute"
if self.fset.__name__ == '<lambda>' or not self.fset.__name__:
self.fset(obj, value)
else:
getattr(obj, self.fset.__name__)(value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "can't delete attribute"
if self.fdel.__name__ == '<lambda>' or not self.fdel.__name__:
self.fdel(obj)
else:
getattr(obj, self.fdel.__name__)()

197
src/_version.py 100644
View File

@ -0,0 +1,197 @@
IN_LONG_VERSION_PY = True
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (build by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.7+ (https://github.com/warner/python-versioneer)
# these strings will be replaced by git during git-archive
git_refnames = "$Format:%d$"
git_full = "$Format:%H$"
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %s" % args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%s', no digits" % ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %s" % ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" %
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
tag_prefix = "python-gnupg-"
parentdir_prefix = "python-gnupg-"
versionfile_source = "src/_version.py"
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
variables = { "refnames": git_refnames, "full": git_full }
ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
if not ver:
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if not ver:
ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
verbose)
if not ver:
ver = default
return ver

50
src/copyleft.py 100644
View File

@ -0,0 +1,50 @@
#-*- encoding: utf-8 -*-
'''
Copyright information for python-gnupg.
'''
copyright = """\
Copyright (C) 2013 Isis Lovecruft.
See LICENSE for details."""
disclaimer = """\
This file is part of python-gnupg, a Python wrapper around GnuPG.
%s
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Affero General Public License for more details.""" % (copyright,)
txcopyright = """\
Where stated, parts of this program were taken from Twisted, which is
licensed as follows:
Twisted, the Framework of Your Internet
Copyright (c) 2001-2013 Twisted Matrix Laboratories.
See LICENSE for details.
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."""

1710
src/gnupg.py 100644

File diff suppressed because it is too large Load Diff

View File

@ -1,547 +0,0 @@
# -*- coding: utf-8 -*-
"""
A test harness for gnupg.py.
Copyright © 2013 Isis Lovecruft.
Copyright © 2008-2013 Vinay Sajip. All rights reserved.
"""
import doctest
import logging
from functools import wraps
import io
import os
import shutil
import sys
import tempfile
import unittest
import gnupg
__author__ = "Isis Lovecruft"
__date__ = "2013-03-02"
ALL_TESTS = True
REPO_DIR = os.getcwd()
TEST_DIR = os.path.join(REPO_DIR, 'keys')
tempfile.tempdir = os.path.join(REPO_DIR, 'temp')
if not os.path.isdir(tempfile.gettempdir()):
os.mkdir(tempfile.gettempdir())
@wraps(tempfile.TemporaryFile)
def _make_tempfile(*args, **kwargs):
return tempfile.TemporaryFile(dir=tempfile.gettempdir(),
*args, **kwargs)
logger = logging.getLogger(__name__)
KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.9 (MingW32)
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
=sqld
-----END PGP PUBLIC KEY BLOCK-----"""
def is_list_with_len(o, n):
return isinstance(o, list) and len(o) == n
def compare_keys(k1, k2):
"""Compare ASCII keys"""
k1 = k1.split('\n')
k2 = k2.split('\n')
del k1[1] # remove version lines
del k2[1]
return k1 != k2
class ResultStringIO(io.StringIO):
def __init__(self):
super(self, io.StringIO).__init__()
def write(self, data):
super(self, io.StringIO).write(unicode(data))
class GPGTestCase(unittest.TestCase):
def setUp(self):
hd = os.path.join(os.getcwd(), 'keys')
if os.path.exists(hd):
self.assertTrue(os.path.isdir(hd),
"Not a directory: %s" % hd)
shutil.rmtree(hd)
self.homedir = hd
self.gpg = gnupg.GPG(gpghome=hd, gpgbinary='gpg')
self.pubring = os.path.join(self.homedir, 'pubring.gpg')
self.secring = os.path.join(self.homedir, 'secring.gpg')
def test_environment(self):
"""Test the environment by ensuring that setup worked"""
hd = self.homedir
self.assertTrue(os.path.exists(hd) and os.path.isdir(hd),
"Not an existing directory: %s" % hd)
def test_gpg_binary(self):
"""Test that 'gpg --version' does not return an error code"""
proc = self.gpg._open_subprocess(['--version'])
result = io.StringIO()
self.gpg._collect_output(proc, result, stdin=proc.stdin)
self.assertEqual(proc.returncode, 0)
def test_gpg_binary_version_str(self):
"""That that 'gpg --version' returns the expected output"""
proc = self.gpg._open_subprocess(['--version'])
result = proc.stdout.read(1024)
expected1 = "Supported algorithms:"
expected2 = "Pubkey:"
expected3 = "Cipher:"
expected4 = "Compression:"
logger.debug("'gpg --version' returned output:n%s" % result)
self.assertGreater(result.find(expected1), 0)
self.assertGreater(result.find(expected2), 0)
self.assertGreater(result.find(expected3), 0)
self.assertGreater(result.find(expected4), 0)
def test_gpg_binary_not_abs(self):
"""Test that a non-absolute path to gpg results in a full path"""
self.assertTrue(os.path.isabs(self.gpg.gpgbinary))
def test_make_args_drop_protected_options(self):
"""Test that unsupported gpg options are dropped"""
self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus']
self.gpg.keyring = self.secring
cmd = self.gpg.make_args(None, False)
expected = ['/usr/bin/gpg',
'--status-fd 2 --no-tty',
'--homedir "/home/isis/code/riseup/python-gnupg/keys"',
'--no-default-keyring --keyring "%s"' % self.secring]
self.assertListEqual(cmd, expected)
def test_make_args(self):
"""Test argument line construction"""
not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet']
self.gpg.options = not_allowed[:-2]
args = self.gpg.make_args(not_allowed[2:], False)
self.assertTrue(len(args) == 4)
for na in not_allowed:
self.assertNotIn(na, args)
def test_list_keys_initial_public(self):
"""Test that initially there are no public keys"""
public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 0),
"Empty list expected...got instead: %s"
% str(public_keys))
def test_list_keys_initial_secret(self):
"""Test that initially there are no secret keys"""
private_keys = self.gpg.list_keys(secret=True)
self.assertTrue(is_list_with_len(private_keys, 0),
"Empty list expected...got instead: %s"
% str(private_keys))
def test_copy_data(self):
"""Test that _copy_data() is able to duplicate byte streams"""
instream = io.BytesIO("This is a string of bytes mapped in memory.")
outstream = str("And this one is just a string.")
def generate_key(self, first_name, last_name, domain, passphrase=None):
"""Generate a key"""
params = {'Key-Type': 'RSA',
'Key-Length': 2048,
'Subkey-Type': 'RSA',
'Subkey-Length': 2048,
'Name-Comment': 'A test user',
'Expire-Date': 0,
'Name-Real': '%s %s' % (first_name, last_name),
'Name-Email': ("%s.%s@%s"
% (first_name, last_name, domain)).lower()}
if passphrase is None:
passphrase = ("%s%s" % (first_name[0], last_name)).lower()
params['Passphrase'] = passphrase
cmd = self.gpg.gen_key_input(**params)
return self.gpg.gen_key(cmd)
def do_key_generation(self):
"""Test that key generation succeeds"""
result = self.generate_key("Barbara", "Brown", "beta.com")
self.assertNotEqual(None, result, "Non-null result")
return result
def test_key_generation_with_invalid_key_type(self):
"""Test that key generation handles invalid key type"""
params = {
'Key-Type': 'INVALID',
'Key-Length': 1024,
'Subkey-Type': 'ELG-E',
'Subkey-Length': 2048,
'Name-Comment': 'A test user',
'Expire-Date': 0,
'Name-Real': 'Test Name',
'Name-Email': 'test.name@example.com',
}
cmd = self.gpg.gen_key_input(**params)
result = self.gpg.gen_key(cmd)
self.assertFalse(result.data, 'Null data result')
self.assertEqual(None, result.fingerprint, 'Null fingerprint result')
def test_key_generation_with_colons(self):
"""Test that key generation handles colons in key fields"""
params = {
'key_type': 'RSA',
'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19',
'name_comment': 'dummy comment',
'name_email': 'test.name@example.com',
}
cmd = self.gpg.gen_key_input(**params)
result = self.gpg.gen_key(cmd)
keys = self.gpg.list_keys()
self.assertEqual(len(keys), 1)
key = keys[0]
uids = key['uids']
self.assertEqual(len(uids), 1)
uid = uids[0]
self.assertEqual(uid, 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19 '
'(dummy comment) <test.name@example.com>')
def test_key_generation_with_empty_value(self):
"""Test that key generation handles empty values"""
params = {
'key_type': 'RSA',
'key_length': 1024,
'name_comment': ' ', # Not added, so default will appear
}
cmd = self.gpg.gen_key_input(**params)
self.assertTrue('\nName-Comment: Generated by gnupg.py\n' in cmd)
params['name_comment'] = 'A'
cmd = self.gpg.gen_key_input(**params)
self.assertTrue('\nName-Comment: A\n' in cmd)
def test_list_keys_after_generation(self):
"""Test that after key generation, the generated key is available"""
self.test_list_keys_initial()
self.do_key_generation()
public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 1),
"1-element list expected")
private_keys = self.gpg.list_keys(secret=True)
self.assertTrue(is_list_with_len(private_keys, 1),
"1-element list expected")
def test_encryption_and_decryption(self):
"""Test that encryption and decryption works"""
logger.debug("test_encryption_and_decryption begins")
key = self.generate_key("Andrew", "Able", "alpha.com",
passphrase="andy")
andrew = key.fingerprint
key = self.generate_key("Barbara", "Brown", "beta.com")
barbara = key.fingerprint
gpg = self.gpg
gpg.encoding = 'latin-1'
if gnupg._py3k:
data = 'Hello, André!'
else:
data = unicode('Hello, André', gpg.encoding)
data = data.encode(gpg.encoding)
edata = str(gpg.encrypt(data, barbara))
self.assertNotEqual(data, edata, "Data must have changed")
ddata = gpg.decrypt(edata, passphrase="bbrown")
if data != ddata.data:
logger.debug("was: %r", data)
logger.debug("new: %r", ddata.data)
self.assertEqual(data, ddata.data, "Round-trip must work")
edata = str(gpg.encrypt(data, [andrew, barbara]))
self.assertNotEqual(data, edata, "Data must have changed")
ddata = gpg.decrypt(edata, passphrase="andy")
self.assertEqual(data, ddata.data, "Round-trip must work")
ddata = gpg.decrypt(edata, passphrase="bbrown")
self.assertEqual(data, ddata.data, "Round-trip must work")
logger.debug("test_encryption_and_decryption ends")
# Test symmetric encryption
data = "chippy was here"
edata = str(gpg.encrypt(data, None, passphrase='bbrown', symmetric=True))
ddata = gpg.decrypt(edata, passphrase='bbrown')
self.assertEqual(data, str(ddata))
def test_public_keyring(self):
"""Test that the public keyring is found in the gpg home directory"""
self.gpg.keyring = self.pubring
self.assertTrue(os.path.isfile(self.pubring))
def test_secret_keyring(self):
"""Test that the secret keyring is found in the gpg home directory"""
self.gpg.keyring = self.secring
self.assertTrue(os.path.isfile(self.secring))
def test_import_and_export(self):
"""Test that key import and export works"""
logger.debug("test_import_and_export begins")
self.test_list_keys_initial()
gpg = self.gpg
result = gpg.import_keys(KEYS_TO_IMPORT)
self.assertEqual(result.summary(), '2 imported')
public_keys = gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 2),
"2-element list expected")
private_keys = gpg.list_keys(secret=True)
self.assertTrue(is_list_with_len(private_keys, 0),
"Empty list expected")
ascii = gpg.export_keys([k['keyid'] for k in public_keys])
self.assertTrue(ascii.find("PGP PUBLIC KEY BLOCK") >= 0,
"Exported key should be public")
ascii = ascii.replace("\r", "").strip()
match = compare_keys(ascii, KEYS_TO_IMPORT)
if match:
logger.debug("was: %r", KEYS_TO_IMPORT)
logger.debug("now: %r", ascii)
self.assertEqual(0, match, "Keys must match")
#Generate a key so we can test exporting private keys
key = self.do_key_generation()
ascii = gpg.export_keys(key.fingerprint, True)
self.assertTrue(ascii.find("PGP PRIVATE KEY BLOCK") >= 0,
"Exported key should be private")
logger.debug("test_import_and_export ends")
def test_import_only(self):
"""Test that key import works"""
logger.debug("test_import_only begins")
self.test_list_keys_initial()
self.gpg.import_keys(KEYS_TO_IMPORT)
public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 2),
"2-element list expected")
private_keys = self.gpg.list_keys(secret=True)
self.assertTrue(is_list_with_len(private_keys, 0),
"Empty list expected")
ascii = self.gpg.export_keys([k['keyid'] for k in public_keys])
self.assertTrue(ascii.find("PGP PUBLIC KEY BLOCK") >= 0,
"Exported key should be public")
ascii = ascii.replace("\r", "").strip()
match = compare_keys(ascii, KEYS_TO_IMPORT)
if match:
logger.debug("was: %r", KEYS_TO_IMPORT)
logger.debug("now: %r", ascii)
self.assertEqual(0, match, "Keys must match")
logger.debug("test_import_only ends")
def test_signature_verification(self):
"""Test that signing and verification works"""
logger.debug("test_signature_verification begins")
key = self.generate_key("Andrew", "Able", "alpha.com")
self.gpg.encoding = 'latin-1'
if gnupg._py3k:
data = 'Hello, André!'
else:
data = unicode('Hello, André', self.gpg.encoding)
data = data.encode(self.gpg.encoding)
sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='bbrown')
self.assertFalse(sig, "Bad passphrase should fail")
sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='aable')
self.assertTrue(sig, "Good passphrase should succeed")
verified = self.gpg.verify(sig.data)
if key.fingerprint != verified.fingerprint:
logger.debug("key: %r", key.fingerprint)
logger.debug("ver: %r", verified.fingerprint)
self.assertEqual(key.fingerprint, verified.fingerprint,
"Fingerprints must match")
self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE)
self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE')
if not os.path.exists('random_binary_data'):
data_file = open('random_binary_data', 'wb')
data_file.write(os.urandom(5120 * 1024))
data_file.close()
data_file = open('random_binary_data', 'rb')
sig = self.gpg.sign_file(data_file, keyid=key.fingerprint,
passphrase='aable')
data_file.close()
self.assertTrue(sig, "File signing should succeed")
try:
file = gnupg._make_binary_stream(sig.data, self.gpg.encoding)
verified = self.gpg.verify_file(file)
except UnicodeDecodeError: #happens in Python 2.6
verified = self.gpg.verify_file(io.BytesIO(sig.data))
if key.fingerprint != verified.fingerprint:
logger.debug("key: %r", key.fingerprint)
logger.debug("ver: %r", verified.fingerprint)
self.assertEqual(key.fingerprint, verified.fingerprint,
"Fingerprints must match")
data_file = open('random_binary_data', 'rb')
sig = self.gpg.sign_file(data_file, keyid=key.fingerprint,
passphrase='aable', detach=True)
data_file.close()
self.assertTrue(sig, "File signing should succeed")
try:
file = gnupg._make_binary_stream(sig.data, self.gpg.encoding)
verified = self.gpg.verify_file(file, 'random_binary_data')
except UnicodeDecodeError: #happens in Python 2.6
verified = self.gpg.verify_file(io.BytesIO(sig.data))
if key.fingerprint != verified.fingerprint:
logger.debug("key: %r", key.fingerprint)
logger.debug("ver: %r", verified.fingerprint)
self.assertEqual(key.fingerprint, verified.fingerprint,
"Fingerprints must match")
logger.debug("test_signature_verification ends")
def test_deletion(self):
"""Test that key deletion works"""
logger.debug("test_deletion begins")
self.gpg.import_keys(KEYS_TO_IMPORT)
public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 2),
"2-element list expected, got %d" % len(public_keys))
self.gpg.delete_keys(public_keys[0]['fingerprint'])
public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 1),
"1-element list expected, got %d" % len(public_keys))
logger.debug("test_deletion ends")
def test_file_encryption_and_decryption(self):
"""Test that encryption/decryption to/from file works"""
logger.debug("test_file_encryption_and_decryption begins")
encfname = _make_tempfile()
logger.debug('Created tempfile for encrypted content: %s' % encfname)
decfname = _make_tempfile()
logger.debug('Created tempfile for decrypted content: f%s' % decfname)
# On Windows, if the handles aren't closed, the files can't be deleted
#os.close(encfno)
#os.close(decfno)
try:
key = self.generate_key("Andrew", "Able", "alpha.com",
passphrase="andy")
andrew = key.fingerprint
key = self.generate_key("Barbara", "Brown", "beta.com")
barbara = key.fingerprint
data = "Hello, world!"
file = gnupg._make_binary_stream(data, self.gpg.encoding)
edata = self.gpg.encrypt_file(file, barbara,
armor=False, output=encfname)
ddata = self.gpg.decrypt_file(efile, passphrase="bbrown",
output=decfname)
encfname.seek(0, 0) # can't use os.SEEK_SET in 2.4
edata = encfname.read()
ddata = decfname.read()
data = data.encode(self.gpg.encoding)
if ddata != data:
logger.debug("was: %r", data)
logger.debug("new: %r", ddata)
self.assertEqual(data, ddata, "Round-trip must work")
except Exception as exc:
logger.warn(exc.message)
logger.debug("test_file_encryption_and_decryption ends")
TEST_GROUPS = {
'basic' : set(['test_environment',
'test_gpg_binary',
'test_gpg_binary_not_abs',
'test_gpg_binary_version_str',
'test_list_keys_initial_public',
'test_list_keys_initial_secret',
'test_make_args_drop_protected_options',
'test_make_args']),
'sign' : set(['test_signature_verification']),
'crypt' : set(['test_encryption_and_decryption',
'test_file_encryption_and_decryption']),
'key' : set(['test_deletion',
'test_import_and_export',
'test_public_keyring',
'test_secret_keyring',
'test_list_keys_after_generation',
'test_key_generation_with_invalid_key_type',
'test_key_generation_with_empty_value',
'test_key_generation_with_colons']),
'import' : set(['test_import_only']),
}
def suite(args=None):
if args is None:
args = sys.argv[1:]
if not args:
result = unittest.TestLoader().loadTestsFromTestCase(GPGTestCase)
want_doctests = False
else:
tests = set()
want_doctests = False
for arg in args:
if arg in TEST_GROUPS:
tests.update(TEST_GROUPS[arg])
elif arg == "doc":
want_doctests = True
else:
print("Ignoring unknown test group %r" % arg)
result = unittest.TestSuite(list(map(GPGTestCase, tests)))
if want_doctests:
result.addTest(doctest.DocTestSuite(gnupg))
return result
def init_logging():
logging.basicConfig(
level=logging.DEBUG, filename="test_gnupg.log",
filemode="a",
format="%(asctime)s %(levelname)-5s %(name)-7s %(threadName)-10s %(message)s")
logging.captureWarnings(True)
logging.logThreads = True
logger.addHandler(logging.StreamHandler(stream=sys.stdout))
#logger.addHandler(logging.RootLogger(logging.DEBUG))
#logger.addHandler(logging.Logger("gnupg.py", level=logging.DEBUG))
def main():
init_logging()
tests = suite()
results = unittest.TextTestRunner(verbosity=3).run(tests)
return not results.wasSuccessful()
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,87 @@
Privacy is necessary for an open society in the electronic age. Privacy is not
secrecy. A private matter is something one doesn't want the whole world to
know, but a secret matter is something one doesn't want anybody to
know. Privacy is the power to selectively reveal oneself to the world.
If two parties have some sort of dealings, then each has a memory of their
interaction. Each party can speak about their own memory of this; how could
anyone prevent it? One could pass laws against it, but the freedom of speech,
even more than privacy, is fundamental to an open society; we seek not to
restrict any speech at all. If many parties speak together in the same forum,
each can speak to all the others and aggregate together knowledge about
individuals and other parties. The power of electronic communications has
enabled such group speech, and it will not go away merely because we might want
it to.
Since we desire privacy, we must ensure that each party to a transaction have
knowledge only of that which is directly necessary for that transaction. Since
any information can be spoken of, we must ensure that we reveal as little as
possible. In most cases personal identity is not salient. When I purchase a
magazine at a store and hand cash to the clerk, there is no need to know who I
am. When I ask my electronic mail provider to send and receive messages, my
provider need not know to whom I am speaking or what I am saying or what others
are saying to me; my provider only need know how to get the message there and
how much I owe them in fees. When my identity is revealed by the underlying
mechanism of the transaction, I have no privacy. I cannot here selectively
reveal myself; I must always reveal myself.
Therefore, privacy in an open society requires anonymous transaction
systems. Until now, cash has been the primary such system. An anonymous
transaction system is not a secret transaction system. An anonymous system
empowers individuals to reveal their identity when desired and only when
desired; this is the essence of privacy.
Privacy in an open society also requires cryptography. If I say something, I
want it heard only by those for whom I intend it. If the content of my speech
is available to the world, I have no privacy. To encrypt is to indicate the
desire for privacy, and to encrypt with weak cryptography is to indicate not
too much desire for privacy. Furthermore, to reveal one's identity with
assurance when the default is anonymity requires the cryptographic signature.
We cannot expect governments, corporations, or other large, faceless
organizations to grant us privacy out of their beneficence. It is to their
advantage to speak of us, and we should expect that they will speak. To try to
prevent their speech is to fight against the realities of
information. Information does not just want to be free, it longs to be
free. Information expands to fill the available storage space. Information is
Rumor's younger, stronger cousin; Information is fleeter of foot, has more
eyes, knows more, and understands less than Rumor.
We must defend our own privacy if we expect to have any. We must come together
and create systems which allow anonymous transactions to take place. People
have been defending their own privacy for centuries with whispers, darkness,
envelopes, closed doors, secret handshakes, and couriers. The technologies of
the past did not allow for strong privacy, but electronic technologies do.
We the Cypherpunks are dedicated to building anonymous systems. We are
defending our privacy with cryptography, with anonymous mail forwarding
systems, with digital signatures, and with electronic money.
Cypherpunks write code. We know that someone has to write software to defend
privacy, and since we can't get privacy unless we all do, we're going to write
it. We publish our code so that our fellow Cypherpunks may practice and play
with it. Our code is free for all to use, worldwide. We don't much care if you
don't approve of the software we write. We know that software can't be
destroyed and that a widely dispersed system can't be shut down.
Cypherpunks deplore regulations on cryptography, for encryption is
fundamentally a private act. The act of encryption, in fact, removes
information from the public realm. Even laws against cryptography reach only so
far as a nation's border and the arm of its violence. Cryptography will
ineluctably spread over the whole globe, and with it the anonymous transactions
systems that it makes possible.
For privacy to be widespread it must be part of a social contract. People must
come and together deploy these systems for the common good. Privacy only
extends so far as the cooperation of one's fellows in society. We the
Cypherpunks seek your questions and your concerns and hope we may engage you so
that we do not deceive ourselves. We will not, however, be moved out of our
course because some may disagree with our goals.
The Cypherpunks are actively engaged in making the networks safer for
privacy. Let us proceed together apace.
Onward.
Eric Hughes <hughes@soda.berkeley.edu>
9 March 1993

138
tests/files/kat.sec 100644
View File

@ -0,0 +1,138 @@
## 1024R primary secret key, usage C, pasphrase 'katpics'
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=66aD
-----END PGP PRIVATE KEY BLOCK-----
## 1024R subkey, usage ES, passphrase 'katpics'
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
TNnjCWTKcw==
=AVxa
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,55 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
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
=sTVP
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,78 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
lQEVBFGh57gBCADRG2Q93/INxPOALefgXj40I8+i9Sc7MblIbZeu3ctAQUL41OXw
zdTo2NKaDH+CcIQrlQZfMnchYnr69M1mDcj2MQy6DqixvNrwJKF4CZwrmFpZRH6W
1NJ5yeU4Q6e0kAXZCSJDFjRaWv68HaNpW8j4XTHdcwmv+ry6APBa+mwhXXqAYmnS
EFu6uk7tEjebnYjnP+J6XxBSHmF7678bvWW7M5IQkG/zxnWpsfLCcrakpxMAJnhZ
EDhZggAl5Rvpd008H6ERJSgLWdZ03qLUutRX0OaJdH/v/aDLDxclYVZtnC1Kg3Jy
2WU+QRP2q2q+xNSJ+smRE1wp+BnMz7qiyt8VABEBAAH+A2UCR05VAbQ5cHl0aG9u
LWdudXBnIHRlc3Qga2V5ICMxIChETyBOT1QgVVNFKSA8dGVzdEBweXRob24tZ251
cGc+iQE+BBMBAgAoBQJRoee4AhsBBQkB4TOABgsJCAcDAgYVCAIJCgsEFgIDAQIe
AQIXgAAKCRCtsugm/7ujSMduCACnfxHLgOJHlfUl9H0VTtccVZetq4W3vSz663zB
7EI3T0UJLK6YlaMCJoraN2mNR20DB26j+vnrgAzV+tQO0cZJkJtZ6zej0IIRAkOa
qjYQKF1QC4gqK07dSHUJJV73CevYyOKofZhfFwly8GckdE5CY6eXdmEU+2pUMpvd
j7ZGh+fOE9zsbsyvU7QSj4nJeRHFsuoiGRw4Rri97mUpnyVc35lgzEvnO4RpeeVS
PBZhxX/8p2flXdGi0WKb4T7W5GVxyXow9HzgGPc/55uy19NOtGLLHqB3R77847ly
BELTV9+oz5mGylR9I463LOS8kLAhMEadwhPdspOxUDww0gF2nQO+BFGh6CsBCACx
d4Sic2mFp9u5rC3LVLY7M01W75uQ3XhFtE/WT+tQ2uTmXF1191AFtC+EB72CK5QU
4nohlNmKBK3c+TshFPYXSQ3qy8KwaKE6b8hdFOEYlgv3E+WF/rRIKrhVde4hlkyu
8GLNBwWXCOIeug0dTDaiKXjLpX5wmK/Pu7btmF0wF9EpKGkiJouVEZrPS5zglM61
wsd9Ka/miswKQEt0gNcaVuz1VY18E5dBKQi4iYuh/EySkfbPAzjZk/OxqEBiADE8
nGULDalOYXRUEWRhVGr+nmdV6jXx7TeclitJf1tOGXl4rcJ90T1csR1t/Vv/SOTC
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
m8XB+to=
=GLJA
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,48 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
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
=bZbj
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,77 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
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
=01Sl
-----END PGP PRIVATE KEY BLOCK-----

1001
tests/test_gnupg.py 100644

File diff suppressed because it is too large Load Diff

656
versioneer.py 100644
View File

@ -0,0 +1,656 @@
#! /usr/bin/python
"""versioneer.py
(like a rocketeer, but for versions)
* https://github.com/warner/python-versioneer
* Brian Warner
* License: Public Domain
* Version: 0.7+
This file helps distutils-based projects manage their version number by just
creating version-control tags.
For developers who work from a VCS-generated tree (e.g. 'git clone' etc),
each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a
version number by asking your version-control tool about the current
checkout. The version number will be written into a generated _version.py
file of your choosing, where it can be included by your __init__.py
For users who work from a VCS-generated tarball (e.g. 'git archive'), it will
compute a version number by looking at the name of the directory created when
te tarball is unpacked. This conventionally includes both the name of the
project and a version number.
For users who work from a tarball built by 'setup.py sdist', it will get a
version number from a previously-generated _version.py file.
As a result, loading code directly from the source tree will not result in a
real version. If you want real versions from VCS trees (where you frequently
update from the upstream repository, or do new development), you will need to
do a 'setup.py version' after each update, and load code from the build/
directory.
You need to provide this code with a few configuration values:
versionfile_source:
A project-relative pathname into which the generated version strings
should be written. This is usually a _version.py next to your project's
main __init__.py file. If your project uses src/myproject/__init__.py,
this should be 'src/myproject/_version.py'. This file should be checked
in to your VCS as usual: the copy created below by 'setup.py
update_files' will include code that parses expanded VCS keywords in
generated tarballs. The 'build' and 'sdist' commands will replace it with
a copy that has just the calculated version string.
versionfile_build:
Like versionfile_source, but relative to the build directory instead of
the source directory. These will differ when your setup.py uses
'package_dir='. If you have package_dir={'myproject': 'src/myproject'},
then you will probably have versionfile_build='myproject/_version.py' and
versionfile_source='src/myproject/_version.py'.
tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all
VCS tags. If your tags look like 'myproject-1.2.0', then you
should use tag_prefix='myproject-'. If you use unprefixed tags
like '1.2.0', this should be an empty string.
parentdir_prefix: a string, frequently the same as tag_prefix, which
appears at the start of all unpacked tarball filenames. If
your tarball unpacks into 'myproject-1.2.0', this should
be 'myproject-'.
To use it:
1: include this file in the top level of your project
2: make the following changes to the top of your setup.py:
import versioneer
versioneer.versionfile_source = 'src/myproject/_version.py'
versioneer.versionfile_build = 'myproject/_version.py'
versioneer.tag_prefix = '' # tags are like 1.2.0
versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0'
3: add the following arguments to the setup() call in your setup.py:
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
4: run 'setup.py update_files', which will create _version.py, and will
append the following to your __init__.py:
from _version import __version__
5: modify your MANIFEST.in to include versioneer.py
6: add both versioneer.py and the generated _version.py to your VCS
"""
import os, sys, re
from distutils.core import Command
from distutils.command.sdist import sdist as _sdist
from distutils.command.build import build as _build
versionfile_source = None
versionfile_build = None
tag_prefix = None
parentdir_prefix = None
VCS = "git"
IN_LONG_VERSION_PY = False
LONG_VERSION_PY = '''
IN_LONG_VERSION_PY = True
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (build by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.7+ (https://github.com/warner/python-versioneer)
# these strings will be replaced by git during git-archive
git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %%s" %% args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %%s (error)" %% args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%%s', no digits" %% ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %%d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %%s" %% ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %%s" %% r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %%s" %% root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %%
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
tag_prefix = "%(TAG_PREFIX)s"
parentdir_prefix = "%(PARENTDIR_PREFIX)s"
versionfile_source = "%(VERSIONFILE_SOURCE)s"
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
variables = { "refnames": git_refnames, "full": git_full }
ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
if not ver:
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if not ver:
ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
verbose)
if not ver:
ver = default
return ver
'''
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %s" % args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%s', no digits" % ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %s" % ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" %
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
import sys
def do_vcs_install(versionfile_source, ipy):
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
run_command([GIT, "add", "versioneer.py"])
run_command([GIT, "add", versionfile_source])
run_command([GIT, "add", ipy])
present = False
try:
f = open(".gitattributes", "r")
for line in f.readlines():
if line.strip().startswith(versionfile_source):
if "export-subst" in line.strip().split()[1:]:
present = True
f.close()
except EnvironmentError:
pass
if not present:
f = open(".gitattributes", "a+")
f.write("%s export-subst\n" % versionfile_source)
f.close()
run_command([GIT, "add", ".gitattributes"])
SHORT_VERSION_PY = """
# This file was generated by 'versioneer.py' (0.7+) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
version_version = '%(version)s'
version_full = '%(full)s'
def get_versions(default={}, verbose=False):
return {'version': version_version, 'full': version_full}
"""
DEFAULT = {"version": "unknown", "full": "unknown"}
def versions_from_file(filename):
versions = {}
try:
f = open(filename)
except EnvironmentError:
return versions
for line in f.readlines():
mo = re.match("version_version = '([^']+)'", line)
if mo:
versions["version"] = mo.group(1)
mo = re.match("version_full = '([^']+)'", line)
if mo:
versions["full"] = mo.group(1)
return versions
def write_to_version_file(filename, versions):
f = open(filename, "w")
f.write(SHORT_VERSION_PY % versions)
f.close()
print("set %s to '%s'" % (filename, versions["version"]))
def get_best_versions(versionfile, tag_prefix, parentdir_prefix,
default=DEFAULT, verbose=False):
# returns dict with two keys: 'version' and 'full'
#
# extract version from first of _version.py, 'git describe', parentdir.
# This is meant to work for developers using a source checkout, for users
# of a tarball created by 'setup.py sdist', and for users of a
# tarball/zipball created by 'git archive' or github's download-from-tag
# feature.
variables = get_expanded_variables(versionfile_source)
if variables:
ver = versions_from_expanded_variables(variables, tag_prefix)
if ver:
if verbose: print("got version from expanded variable %s" % ver)
return ver
ver = versions_from_file(versionfile)
if ver:
if verbose: print("got version from file %s %s" % (versionfile, ver))
return ver
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if ver:
if verbose: print("got version from git %s" % ver)
return ver
ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose)
if ver:
if verbose: print("got version from parentdir %s" % ver)
return ver
if verbose: print("got version from default %s" % ver)
return default
def get_versions(default=DEFAULT, verbose=False):
assert versionfile_source is not None, "please set versioneer.versionfile_source"
assert tag_prefix is not None, "please set versioneer.tag_prefix"
assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix"
return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix,
default=default, verbose=verbose)
def get_version(verbose=False):
return get_versions(verbose=verbose)["version"]
class cmd_version(Command):
description = "report generated version string"
user_options = []
boolean_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
ver = get_version(verbose=True)
print("Version is currently: %s" % ver)
class cmd_build(_build):
def run(self):
versions = get_versions(verbose=True)
_build.run(self)
# now locate _version.py in the new build/ directory and replace it
# with an updated value
target_versionfile = os.path.join(self.build_lib, versionfile_build)
print("UPDATING %s" % target_versionfile)
os.unlink(target_versionfile)
f = open(target_versionfile, "w")
f.write(SHORT_VERSION_PY % versions)
f.close()
class cmd_sdist(_sdist):
def run(self):
versions = get_versions(verbose=True)
self._versioneer_generated_versions = versions
# unless we update this, the command will keep using the old version
self.distribution.metadata.version = versions["version"]
return _sdist.run(self)
def make_release_tree(self, base_dir, files):
_sdist.make_release_tree(self, base_dir, files)
# now locate _version.py in the new base_dir directory (remembering
# that it may be a hardlink) and replace it with an updated value
target_versionfile = os.path.join(base_dir, versionfile_source)
print("UPDATING %s" % target_versionfile)
os.unlink(target_versionfile)
f = open(target_versionfile, "w")
f.write(SHORT_VERSION_PY % self._versioneer_generated_versions)
f.close()
INIT_PY_SNIPPET = """
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
"""
class cmd_update_files(Command):
description = "modify __init__.py and create _version.py"
user_options = []
boolean_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py")
print(" creating %s" % versionfile_source)
f = open(versionfile_source, "w")
f.write(LONG_VERSION_PY % {"DOLLAR": "$",
"TAG_PREFIX": tag_prefix,
"PARENTDIR_PREFIX": parentdir_prefix,
"VERSIONFILE_SOURCE": versionfile_source,
})
f.close()
try:
old = open(ipy, "r").read()
except EnvironmentError:
old = ""
if INIT_PY_SNIPPET not in old:
print(" appending to %s" % ipy)
f = open(ipy, "a")
f.write(INIT_PY_SNIPPET)
f.close()
else:
print(" %s unmodified" % ipy)
do_vcs_install(versionfile_source, ipy)
def get_cmdclass():
return {'version': cmd_version,
'update_files': cmd_update_files,
'build': cmd_build,
'sdist': cmd_sdist,
}