diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 7204f9e..b5cac3a 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -21,15 +21,15 @@ on: jobs: linux-validate: name: Validate on Linux - Python ${{ matrix.python }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: - python: [ '3.6', '3.8', '3.12' ] + python: [ '3.7', '3.8', '3.12' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x64 @@ -42,12 +42,12 @@ jobs: runs-on: windows-latest strategy: matrix: - python: [ '3.6', '3.8', '3.12' ] + python: [ '3.7', '3.8', '3.12' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x64 diff --git a/example/example.py b/example/example.py index b9db850..815aecc 100644 --- a/example/example.py +++ b/example/example.py @@ -8,7 +8,7 @@ def __init__(self): def emit(self, record): logstr = self.format(record) - print logstr + print(logstr) patchlog = logging.getLogger("patch") patchlog.handlers = [] diff --git a/patch_ng.py b/patch_ng.py index e1114fb..ef2af6c 100755 --- a/patch_ng.py +++ b/patch_ng.py @@ -28,56 +28,23 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from __future__ import print_function - __author__ = "Conan.io " __version__ = "1.18.0" __license__ = "MIT" __url__ = "https://github.com/conan-io/python-patch" +import codecs import copy +import io import logging -import re -import tempfile -import codecs - -# cStringIO doesn't support unicode in 2.5 -try: - from StringIO import StringIO -except ImportError: - from io import BytesIO as StringIO # python 3 -try: - import urllib2 as urllib_request -except ImportError: - import urllib.request as urllib_request - -from os.path import exists, isfile, abspath import os import posixpath +import re import shutil -import sys import stat - - -PY3K = sys.version_info >= (3, 0) - -# PEP 3114 -if not PY3K: - compat_next = lambda gen: gen.next() -else: - compat_next = lambda gen: gen.__next__() - -def tostr(b): - """ Python 3 bytes encoder. Used to print filename in - diffstat output. Assumes that filenames are in utf-8. - """ - if not PY3K: - return b - - # [ ] figure out how to print non-utf-8 filenames without - # information loss - return b.decode('utf-8') - +import tempfile +import urllib.request +from os.path import exists, isfile, abspath #------------------------------------------------ # Logging is controlled by logger named after the @@ -90,22 +57,10 @@ def tostr(b): warning = logger.warning error = logger.error -class NullHandler(logging.Handler): - """ Copied from Python 2.7 to avoid getting - `No handlers could be found for logger "patch"` - http://bugs.python.org/issue16539 - """ - def handle(self, record): - pass - def emit(self, record): - pass - def createLock(self): - self.lock = None - streamhandler = logging.StreamHandler() # initialize logger itself -logger.addHandler(NullHandler()) +logger.addHandler(logging.NullHandler()) debugmode = False @@ -194,11 +149,10 @@ def fromfile(filename): """ patchset = PatchSet() debug("reading %s" % filename) - fp = open(filename, "rb") - res = patchset.parse(fp) - fp.close() - if res == True: - return patchset + with open(filename, "rb") as fp: + res = patchset.parse(fp) + if res == True: + return patchset return False @@ -206,7 +160,7 @@ def fromstring(s): """ Parse text string and return PatchSet() object (or False if parsing fails) """ - ps = PatchSet( StringIO(s) ) + ps = PatchSet( io.BytesIO(s) ) if ps.errors == 0: return ps return False @@ -217,7 +171,7 @@ def fromurl(url): if an error occured. Note that this also can throw urlopen() exceptions. """ - ps = PatchSet( urllib_request.urlopen(url) ) + ps = PatchSet( urllib.request.urlopen(url) ) if ps.errors == 0: return ps return False @@ -261,15 +215,6 @@ def decode_text(text): return text.decode("utf-8", "ignore") # Ignore not compatible characters -def to_file_bytes(content): - if PY3K: - if not isinstance(content, bytes): - content = bytes(content, "utf-8") - elif isinstance(content, unicode): - content = content.encode("utf-8") - return content - - def load(path, binary=False): """ Loads a file content """ with open(path, 'rb') as handle: @@ -290,7 +235,9 @@ def save(path, content, only_if_modified=False): except Exception: pass - new_content = to_file_bytes(content) + new_content = content + if not isinstance(content, bytes): + new_content = bytes(content, "utf-8") if only_if_modified and os.path.exists(path): old_content = load(path, binary=True) @@ -328,8 +275,7 @@ def __init__(self): self.type = None def __iter__(self): - for h in self.hunks: - yield h + return iter(self.hunks) class PatchSet(object): @@ -359,8 +305,7 @@ def __len__(self): return len(self.items) def __iter__(self): - for i in self.items: - yield i + return iter(self.items) def parse(self, stream): """ parse unified diff @@ -394,7 +339,7 @@ def next(self): return False try: - self._lineno, self._line = compat_next(super(wrapumerate, self)) + self._lineno, self._line = super(wrapumerate, self).__next__() except StopIteration: self._exhausted = True self._line = False @@ -902,7 +847,7 @@ def diffstat(self): #print(iratio, dratio, iwidth, dwidth, histwidth) hist = "+"*int(iwidth) + "-"*int(dwidth) # -- /calculating +- histogram -- - output += (format % (tostr(names[i]), str(insert[i] + delete[i]), hist)) + output += (format % (names[i].decode('utf-8'), str(insert[i] + delete[i]), hist)) output += (" %d files changed, %d insertions(+), %d deletions(-), %+d bytes" % (len(names), sum(insert), sum(delete), delta)) @@ -1270,15 +1215,11 @@ def get_line(): def write_hunks(self, srcname, tgtname, hunks): - src = open(srcname, "rb") - tgt = open(tgtname, "wb") - - debug("processing target file %s" % tgtname) - - tgt.writelines(self.patch_stream(src, hunks)) + with open(srcname, "rb") as src, open(tgtname, "wb") as tgt: + debug("processing target file %s" % tgtname) + + tgt.writelines(self.patch_stream(src, hunks)) - tgt.close() - src.close() # [ ] TODO: add test for permission copy shutil.copymode(srcname, tgtname) return True diff --git a/tests/06nested/tests/app/EVENT_LOOP.py b/tests/06nested/tests/app/EVENT_LOOP.py index 782de99..28c7e9e 100644 --- a/tests/06nested/tests/app/EVENT_LOOP.py +++ b/tests/06nested/tests/app/EVENT_LOOP.py @@ -21,8 +21,8 @@ class EVENT_LOOP(unittest.TestCase): def t_scheduled(self, interval, iterations, sleep_time=0): - print 'Test interval=%s, iterations=%s, sleep=%s' % (interval, - iterations, sleep_time) + print('Test interval=%s, iterations=%s, sleep=%s' % (interval, + iterations, sleep_time)) warmup_iterations = iterations self.last_t = 0. @@ -76,6 +76,6 @@ def test_d01_50(self): if __name__ == '__main__': if pyglet.version != '1.2dev': - print 'Wrong version of pyglet imported; please check your PYTHONPATH' + print('Wrong version of pyglet imported; please check your PYTHONPATH') else: unittest.main() diff --git a/tests/run_tests.py b/tests/run_tests.py index 6c16ef2..cc1814b 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -36,15 +36,10 @@ import re import shutil import unittest -import copy import stat from os import listdir, chmod from os.path import abspath, dirname, exists, join, isdir, isfile from tempfile import mkdtemp -try: - getcwdu = os.getcwdu -except AttributeError: - getcwdu = os.getcwd # python 3, where getcwd always returns a unicode object verbose = False if "-v" in sys.argv or "--verbose" in sys.argv: @@ -150,7 +145,7 @@ def _run_test(self, testname): # 3. # test utility as a whole patch_tool = join(dirname(TESTS), "patch_ng.py") - save_cwd = getcwdu() + save_cwd = os.getcwd() os.chdir(tmpdir) extra = "-f" if "10fuzzy" in testname else "" if verbose: @@ -204,7 +199,7 @@ def create_closure(): class TestCheckPatched(unittest.TestCase): def setUp(self): - self.save_cwd = getcwdu() + self.save_cwd = os.getcwd() os.chdir(TESTS) def tearDown(self): @@ -355,7 +350,7 @@ def test(self): class TestPatchApply(unittest.TestCase): def setUp(self): - self.save_cwd = getcwdu() + self.save_cwd = os.getcwd() self.tmpdir = mkdtemp(prefix=self.__class__.__name__) os.chdir(self.tmpdir) @@ -457,6 +452,41 @@ def test_unlink_backup_windows(self): self.assertTrue(os.stat(some_file).st_mode, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) +class TestHugePatchFile(unittest.TestCase): + + def setUp(self): + self.save_cwd = os.getcwd() + self.tmpdir = mkdtemp(prefix=self.__class__.__name__) + os.chdir(self.tmpdir) + self.huge_patchfile = self._create_huge_patchfile(self.tmpdir) + + def _create_huge_patchfile(self, tmpdir): + """ Create a patch file with ~30MB of data + """ + hugefile = join(tmpdir, 'hugefile') + with open(hugefile, 'wb') as f: + for i in range(2500000): + f.write(b'Line %d\n' % i) + huge_patchfile = join(tmpdir, 'huge.patch') + with open(huge_patchfile, 'wb') as f: + f.write(b'--- a/hugefile\n') + f.write(b'+++ b/hugefile\n') + f.write(b'@@ -1,2500000 +1,2500000 @@\n') + for i in range(2500000): + f.write(b' Line %d\n' % i) + return huge_patchfile + + def tearDown(self): + os.chdir(self.save_cwd) + remove_tree_force(self.tmpdir) + + def test_apply_huge_patch(self): + """ Test that a huge patch file can be applied without issues + """ + pto = patch_ng.fromfile(self.huge_patchfile) + self.assertTrue(pto.apply(root=self.tmpdir)) + + class TestHelpers(unittest.TestCase): # unittest setting longMessage = True