Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for textbox -> txbxContent #203

Merged
merged 21 commits into from
May 16, 2016
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
**dev**

- Textboxes have been implemented. We no longer lose the content inside of
them.

**0.9.6**

- Fixed issue in PyDocX CLI tool and added new test cases for the same
Expand Down
19 changes: 18 additions & 1 deletion pydocx/export/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
NumberingSpan,
NumberingSpanBuilder,
)
from pydocx.openxml import wordprocessing, vml
from pydocx.openxml import wordprocessing, vml, markup_compatibility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alphabetize?

from pydocx.openxml.packaging import WordprocessingDocument


Expand Down Expand Up @@ -67,6 +67,10 @@ def __init__(self, path):
wordprocessing.EmbeddedObject: self.export_embedded_object,
NumberingSpan: self.export_numbering_span,
NumberingItem: self.export_numbering_item,
markup_compatibility.AlternateContent: self.export_alternate_content,
markup_compatibility.Fallback: self.export_fallback,
wordprocessing.Textbox: self.export_textbox,
wordprocessing.TxBxContent: self.export_textbox_content,
}
self.field_type_to_export_func_map = {
'HYPERLINK': getattr(self, 'export_field_hyperlink', None),
Expand Down Expand Up @@ -531,3 +535,16 @@ def export_field_char(self, field_char):

def export_field_code(self, field_code):
pass

def export_textbox(self, textbox):
return self.yield_nested(textbox.children, self.export_node)

def export_textbox_content(self, textbox_content):
return self.yield_nested(textbox_content.children, self.export_node)

# Markup Compatibility exporters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we no longer need this comment.

def export_alternate_content(self, alternate_content):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we call this export_markup_compatibility_alternative_content? I'd rather have a longer-than-ideal name that explains the concept than a name that needs a comment.

return self.yield_nested(alternate_content.children, self.export_node)

def export_fallback(self, fallback):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export_markup_compatibility_fallback?

return self.yield_nested(fallback.children, self.export_node)
18 changes: 17 additions & 1 deletion pydocx/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
unicode_literals,
)

import importlib
import inspect
from collections import defaultdict

Expand Down Expand Up @@ -120,9 +121,24 @@ class ParkingLot(XmlModel):
def __init__(self, *types, **kwargs):
default = kwargs.pop('default', [])
super(XmlCollection, self).__init__(self, default=default)
self.types = set(types)
self._types = types
self._name_to_type_map = None

@property
def types(self):
return set(self._set_types(*self._types))

def _set_types(self, *types):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to consider caching this result? Would be expensive to have to run it more than once, if stuff is needing to be imported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had considered it. Do you recall if pydocx has a built in caching tool? Or should I do it the old fashioned way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought. This gets called for each instance. I might need to make types be a class method and cache it there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a class method would actually fix this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My research leads me to believe that no caching is needed (it won't actually change anything).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify what you researched, Jason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I printed the types in this method and could only get that printing the first time it was used in a test. The second test did not show any of the printing.

base_path = 'pydocx.openxml.{0}'
for _type in types:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like moving all of the imports to be string-based. That removes some of the developer-friendly niceness of immediately knowing when you typo'd an import. Django's approach of mostly using actual imports, but supporting string-based imports where necessary, seems like the best of both worlds. We use strings when we need it, and get immediate feedback on import fails for all of the other cases.

try:
path, klass, = _type.rsplit('.', 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A stray , ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:(

except AttributeError:
yield _type
else:
module = importlib.import_module(base_path.format(path))
yield getattr(module, klass)

@property
def name_to_type_map(self):
if self._name_to_type_map is None:
Expand Down
7 changes: 7 additions & 0 deletions pydocx/openxml/markup_compatibility/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydocx.openxml.markup_compatibility.alternate_content import AlternateContent
from pydocx.openxml.markup_compatibility.fallback import Fallback

__all__ = [
'AlternateContent',
'Fallback',
]
13 changes: 13 additions & 0 deletions pydocx/openxml/markup_compatibility/alternate_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# coding: utf-8
from __future__ import (
absolute_import,
print_function,
unicode_literals,
)

from pydocx.models import XmlModel, XmlCollection


class AlternateContent(XmlModel):
XML_TAG = 'AlternateContent'
children = XmlCollection('markup_compatibility.Fallback')
13 changes: 13 additions & 0 deletions pydocx/openxml/markup_compatibility/fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# coding: utf-8
from __future__ import (
absolute_import,
print_function,
unicode_literals,
)

from pydocx.models import XmlModel, XmlCollection


class Fallback(XmlModel):
XML_TAG = 'Fallback'
children = XmlCollection('wordprocessing.Picture')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know what other child types Fallback supports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will be dealing with that in #204 as I mentioned during stand up :)

I don't plan to push a new release until both this and 203 are done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this comment?
#TODO in #204- actually include all of the children defined by the spec

3 changes: 1 addition & 2 deletions pydocx/openxml/vml/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
)

from pydocx.models import XmlModel, XmlCollection, XmlAttribute
from pydocx.openxml.vml.image_data import ImageData


class Shape(XmlModel):
XML_TAG = 'shape'

style = XmlAttribute()
children = XmlCollection(ImageData)
children = XmlCollection('vml.ImageData', 'wordprocessing.Textbox')

# TODO perhaps we could have a prepare_style, or clean_style convention?

Expand Down
3 changes: 3 additions & 0 deletions pydocx/openxml/wordprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from pydocx.openxml.wordprocessing.table_cell_properties import TableCellProperties # noqa
from pydocx.openxml.wordprocessing.table_row import TableRow
from pydocx.openxml.wordprocessing.text import Text
from pydocx.openxml.wordprocessing.textbox import Textbox, TxBxContent

__all__ = [
'AbstractNum',
Expand Down Expand Up @@ -82,4 +83,6 @@
'TableCell',
'TableRow',
'Text',
'Textbox',
'TxBxContent',
]
9 changes: 3 additions & 6 deletions pydocx/openxml/wordprocessing/deleted_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
)

from pydocx.models import XmlModel, XmlCollection
from pydocx.openxml.wordprocessing.run import Run
from pydocx.openxml.wordprocessing.smart_tag_run import SmartTagRun


class DeletedRun(XmlModel):
XML_TAG = 'del'

children = XmlCollection(
Run,
SmartTagRun,
'wordprocessing.Run',
'wordprocessing.SmartTagRun',
'wordprocessing.DeletedRun',
# TODO Needs InsertedRun
)

DeletedRun.children.types.add(DeletedRun)
9 changes: 3 additions & 6 deletions pydocx/openxml/wordprocessing/inserted_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
)

from pydocx.models import XmlModel, XmlCollection
from pydocx.openxml.wordprocessing.run import Run
from pydocx.openxml.wordprocessing.smart_tag_run import SmartTagRun


class InsertedRun(XmlModel):
XML_TAG = 'ins'

children = XmlCollection(
Run,
SmartTagRun,
'wordprocessing.Run',
'wordprocessing.SmartTagRun',
'wordprocessing.InsertedRun',
# TODO Needs DeletedRun
)

InsertedRun.children.types.add(InsertedRun)
37 changes: 13 additions & 24 deletions pydocx/openxml/wordprocessing/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,6 @@

from pydocx.models import XmlModel, XmlCollection, XmlChild
from pydocx.openxml.wordprocessing.run_properties import RunProperties
from pydocx.openxml.wordprocessing.br import Break
from pydocx.openxml.wordprocessing.drawing import Drawing
from pydocx.openxml.wordprocessing.field_char import FieldChar
from pydocx.openxml.wordprocessing.field_code import FieldCode
from pydocx.openxml.wordprocessing.picture import Picture
from pydocx.openxml.wordprocessing.no_break_hyphen import NoBreakHyphen
from pydocx.openxml.wordprocessing.text import Text
from pydocx.openxml.wordprocessing.tab_char import TabChar
from pydocx.openxml.wordprocessing.deleted_text import DeletedText
from pydocx.openxml.wordprocessing.footnote_reference import FootnoteReference
from pydocx.openxml.wordprocessing.footnote_reference_mark import FootnoteReferenceMark
from pydocx.openxml.wordprocessing.embedded_object import EmbeddedObject
from pydocx.util.memoize import memoized


Expand All @@ -29,18 +17,19 @@ class Run(XmlModel):
properties = XmlChild(type=RunProperties)

children = XmlCollection(
EmbeddedObject,
TabChar,
Break,
NoBreakHyphen,
Text,
Drawing,
Picture,
DeletedText,
FootnoteReference,
FootnoteReferenceMark,
FieldChar,
FieldCode,
'wordprocessing.EmbeddedObject',
'wordprocessing.TabChar',
'wordprocessing.Break',
'wordprocessing.NoBreakHyphen',
'wordprocessing.Text',
'wordprocessing.Drawing',
'wordprocessing.Picture',
'wordprocessing.DeletedText',
'wordprocessing.FootnoteReference',
'wordprocessing.FootnoteReferenceMark',
'wordprocessing.FieldChar',
'wordprocessing.FieldCode',
'markup_compatibility.AlternateContent',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you doing this just to eliminate the import clutter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, one concern I have about this is that we're not validating the types until the class is actually used, which means errors could be hidden. Before, that validation would occur at initial run time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we have a test on each of these nodes, it doesn't matter if it happens at run time vs compile time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you verify that is the case? We'll also need to ensure that any future additions have the requisite test cases as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traceback (most recent call last):
  File "/home/jason/third_party_apps/pydocx/tests/test_main.py", line 70, in test_file_handles_to_docx_are_released
    result = main(['--html', input_docx.name, output.name])
  File "/home/jason/third_party_apps/pydocx/pydocx/__main__.py", line 44, in main
    return convert(output_type, docx_path, output_path)
  File "/home/jason/third_party_apps/pydocx/pydocx/__main__.py", line 15, in convert
    output = PyDocX.to_html(docx_path)
  File "/home/jason/third_party_apps/pydocx/pydocx/pydocx.py", line 13, in to_html
    return PyDocXHTMLExporter(path_or_stream).export()
  File "/home/jason/third_party_apps/pydocx/pydocx/export/html.py", line 211, in export
    for result in super(PyDocXHTMLExporter, self).export()
  File "/home/jason/third_party_apps/pydocx/pydocx/export/html.py", line 209, in <genexpr>
    result.to_html() if isinstance(result, HtmlTag)
  File "/home/jason/third_party_apps/pydocx/pydocx/export/base.py", line 110, in export
    document = self.main_document_part.document
  File "/home/jason/third_party_apps/pydocx/pydocx/openxml/packaging/main_document_part.py", line 49, in document
    self._document = self.load_document()
  File "/home/jason/third_party_apps/pydocx/pydocx/openxml/packaging/main_document_part.py", line 53, in load_document
    self._document = Document.load(self.root_element, container=self)
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 352, in load
    kwargs[field_name] = handler(child)
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 310, in child_handler
    return field.type.load(value, **load_kwargs)
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 370, in load
    item = handler(child, **load_kwargs)
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 370, in load
    item = handler(child, **load_kwargs)
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 339, in load
    for tag_name in field.name_to_type_map.keys():
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 146, in name_to_type_map
    for type_spec in self.types:
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 129, in types
    return set(self._set_types(*self._types))
  File "/home/jason/third_party_apps/pydocx/pydocx/models.py", line 139, in _set_types
    module = importlib.import_module(base_path.format(path))
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
ImportError: No module named rordprocessing

======================================================================
FAIL: test_cli_convert_to_html_status_code (tests.test_main.MainTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/jason/third_party_apps/pydocx/tests/test_main.py", line 99, in test_cli_convert_to_html_status_code
    self.assertEqual(result, 0)
AssertionError: 1 != 0

----------------------------------------------------------------------
Ran 502 tests in 1.186s

FAILED (SKIP=4, errors=326, failures=1)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think testcase coverage is sufficient to mitigate that concern. We're not the only users of PyDocx. We also need to think about the developer experience for other folks that will be using and extending the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to solve your problem then. If we want to allow strings (which I am +1 on), then we have to rely on tests. If we don't want strings, then we have to update things in odd places.

)

def get_style_chain_stack(self):
Expand Down
6 changes: 2 additions & 4 deletions pydocx/openxml/wordprocessing/smart_tag_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
)

from pydocx.models import XmlModel, XmlCollection
from pydocx.openxml.wordprocessing.run import Run


class SmartTagRun(XmlModel):
XML_TAG = 'smartTag'

children = XmlCollection(
Run,
'wordprocessing.Run',
'wordprocessing.SmartTagRun',
)

SmartTagRun.children.types.add(SmartTagRun)
8 changes: 1 addition & 7 deletions pydocx/openxml/wordprocessing/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@
from collections import defaultdict

from pydocx.models import XmlModel, XmlCollection
from pydocx.openxml.wordprocessing.table_cell import TableCell
from pydocx.openxml.wordprocessing.table_row import TableRow


class Table(XmlModel):
XML_TAG = 'tbl'

rows = XmlCollection(
TableRow,
'wordprocessing.TableRow',
)

def calculate_table_cell_spans(self):
Expand Down Expand Up @@ -46,7 +44,3 @@ def calculate_table_cell_spans(self):
if active_rowspan_for_column:
cell_to_rowspan_count[active_rowspan_for_column] += 1 # noqa
return dict(cell_to_rowspan_count)


# Python makes defining nested class hierarchies at the global level difficult
TableCell.children.types.add(Table)
5 changes: 2 additions & 3 deletions pydocx/openxml/wordprocessing/table_cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
)

from pydocx.models import XmlModel, XmlCollection, XmlChild
from pydocx.openxml.wordprocessing.paragraph import Paragraph
from pydocx.openxml.wordprocessing.table_cell_properties import TableCellProperties # noqa


Expand All @@ -16,6 +15,6 @@ class TableCell(XmlModel):
properties = XmlChild(type=TableCellProperties)

children = XmlCollection(
Paragraph,
# Table is added in wordprocessing.table
'wordprocessing.Paragraph',
'wordprocessing.Table',
)
23 changes: 23 additions & 0 deletions pydocx/openxml/wordprocessing/textbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# coding: utf-8
from __future__ import (
absolute_import,
print_function,
unicode_literals,
)

from pydocx.models import XmlModel, XmlCollection


class TxBxContent(XmlModel):
XML_TAG = 'txbxContent'
children = XmlCollection(
'wordprocessing.Paragraph',
)


class Textbox(XmlModel):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guy should really live in vml instead of wordprocessing :( I'll handle this after the code review.

XML_TAG = 'textbox'

children = XmlCollection(
TxBxContent,
)
15 changes: 8 additions & 7 deletions tests/export/test_docx.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,35 @@ def convert(path, *args, **kwargs):

class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory):
cases = (
'read_same_image_multiple_times',
'all_configured_styles',
'export_from_googledocs',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this is alphabetizing.

'external_image',
'has_missing_image',
'has_missing_image',
'has_title',
'inline_tags',
'has_missing_image',
'justification',
'list_in_table',
'external_image',
'export_from_googledocs',
'has_missing_image',
'lists_with_styles',
'missing_numbering',
'missing_style',
'nested_lists',
'nested_table_rowspan',
'nested_tables',
'no_break_hyphen',
'read_same_image_multiple_times',
'rotate_image',
'shift_enter',
'simple',
'simple_lists',
'simple_table',
'special_chars',
'styled_bolding',
'table_col_row_span',
'table_with_multi_rowspan',
'tables_in_lists',
'textbox',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is new.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO for ourselves, we should avoid adding test cases via this method and prefer adding explicit XML test cases. Being able to test against a test file is good for new developers, since it eliminates some of the overhead in getting an issue fixed. It's been by goal to gradually reduce this list down by converting them into explicit XML test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would disagree. Way back when, I wanted to have an actual docx file that was being tested for each case we were working on. It's very easy to build an XML snippet that can't actually exist in a docx file. These are smoke tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not scalable to use full documents for each specific case. You will end up with thousands of poorly named, poorly organized files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not trying to say each docx file has one and only one thing it's testing. I want to make sure that newly considered nodes exist in a docx file that we are parsing in a test. This ensures that, at least when the new node is first added, that we have a real life example that we know now works. I feel pretty strongly about having each of the nodes being processed in our tests from docx files. Mostly because back when I would fake it (in XML), I created more than a few instances of XML that could not exist in an actual docx. Meaning I was writing fixes for things that could not happen. Meaning I quite likely didn't solve the problem.

'track_changes_on',
'table_with_multi_rowspan',
'rotate_image'
)

@raises(MalformedDocxException)
Expand Down
Binary file added tests/fixtures/textbox.docx
Binary file not shown.
5 changes: 5 additions & 0 deletions tests/fixtures/textbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<p>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact, if you look at the XML for this, it is actually nested paragraphs. Not really sure how to stop it from doing the outside one. Or even if that will be the correct solution in the general case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably going to be bad. I think we need to figure out a solution for this. I suspect we will also want additional test cases that consist of having text within the parent level paragraph as well. For example:

<p>
<r><t>Foo</t></r>
<r> ... (textbox) </r>
<r><t>Bar</t></r>
</p>

IMO this needs to be de-normalized into three separate paragraphs for HTML purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see what I can do, but I don't know if I know how to do something like this anymore.

<p>AAA</p>
<p>BBB</p>
<p>CCCDDD</p>
</p>
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ commands =
deps =
-rrequirements/testing.txt
defusedxml: defusedxml==0.4.1
py26: importlib
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( Might be time to drop support for python 2.6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on dropping python 2.6 support if it makes anything easier. We could do that in this PR or in a separate PR and then bump the major version for the next release.


[testenv:docs]
commands =
Expand Down