Skip to content

Commit 64a6a53

Browse files
committed
Initial commit
0 parents  commit 64a6a53

12 files changed

+304
-0
lines changed

.editorconfig

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = tab
6+
indent_size = 4
7+
insert_final_newline = true
8+
end_of_line = lf
9+
10+
[*.{yml,yaml}]
11+
indent_style = space
12+
indent_size = 2

.github/.templateMarker

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
KOLANICH/python_project_boilerplate.py

.github/dependabot.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "daily"
7+
allow:
8+
- dependency-type: "all"

.github/workflows/CI.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: CI
2+
on:
3+
push:
4+
branches: [master]
5+
pull_request:
6+
branches: [master]
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-22.04
11+
steps:
12+
- name: typical python workflow
13+
uses: KOLANICH-GHActions/typical-python-workflow@master
14+
with:
15+
github_token: ${{ secrets.GITHUB_TOKEN }}

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__pycache__
2+
*.pyc
3+
*.pyo
4+
./slicerangetools.egg-info
5+
./build
6+
./dist
7+
./.eggs
8+

.gitlab-ci.yml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest
2+
stages:
3+
- build
4+
- test
5+
6+
variables:
7+
DOCKER_DRIVER: overlay2
8+
SAST_ANALYZER_IMAGE_TAG: latest
9+
SAST_DISABLE_DIND: "true"
10+
11+
include:
12+
- template: SAST.gitlab-ci.yml
13+
#- template: DAST.gitlab-ci.yml
14+
#- template: License-Management.gitlab-ci.yml
15+
#- template: Container-Scanning.gitlab-ci.yml
16+
#- template: Dependency-Scanning.gitlab-ci.yml
17+
- template: Code-Quality.gitlab-ci.yml
18+
19+
20+
build:
21+
tags:
22+
- shared
23+
stage: build
24+
variables:
25+
GIT_DEPTH: "1"
26+
PYTHONUSERBASE: ${CI_PROJECT_DIR}/python_user_packages
27+
28+
before_script:
29+
- export PATH="$PATH:$PYTHONUSERBASE/bin" # don't move into `variables`
30+
31+
cache:
32+
paths:
33+
- $PYTHONUSERBASE
34+
35+
script:
36+
- python3 setup.py bdist_wheel
37+
- mkdir wheels
38+
- mv ./dist/*.whl ./wheels/sideBySideDiff-0.CI-py3-none-any.whl
39+
- pip3 install --user --upgrade ./wheels/sideBySideDiff-0.CI-py3-none-any.whl
40+
#- coverage run --source=sideBySideDiff -m pytest --junitxml=./rspec.xml ./tests/tests.py
41+
#- coverage report -m || true
42+
#- coveralls || true
43+
#- codecov || true
44+
45+
artifacts:
46+
paths:
47+
- wheels
48+
#reports:
49+
# junit: rspec.xml

Code_Of_Conduct.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No codes of conduct!

MANIFEST.in

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include UNLICENSE
2+
include *.md
3+
include tests
4+
include .editorconfig

ReadMe.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sideBySideDiff.py [![Unlicensed work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/)
2+
==================
3+
~~[![GitLab Build Status](https://gitlab.com/KOLANICH/sideBySideDiff.py/badges/master/pipeline.svg)](https://gitlab.com/KOLANICH/sideBySideDiff.py/pipelines/master/latest)~~
4+
~~![GitLab Coverage](https://gitlab.com/KOLANICH/sideBySideDiff.py/badges/master/coverage.svg)~~
5+
[![Libraries.io Status](https://img.shields.io/librariesio/github/KOLANICH/sideBySideDiff.py.svg)](https://libraries.io/github/KOLANICH/sideBySideDiff.py)
6+
[![Code style: antiflash](https://img.shields.io/badge/code%20style-antiflash-FFF.svg)](https://codeberg.org/KOLANICH-tools/antiflash.py)
7+
8+
This is a library to create 2-way side-by-side diffs and show them in CLI using ANSI color codes.
9+

UNLICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <https://unlicense.org/>

pyproject.toml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[build-system]
2+
requires = ["setuptools>=61.2.0", "wheel", "setuptools_scm[toml]>=3.4.3"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "sideBySideDiff"
7+
authors = [{name = "KOLANICH"}]
8+
description = "A library to create 2-way side-by-side diffs for showing them in console"
9+
readme = "ReadMe.md"
10+
keywords = ["diff", "2-way-diff", "compare", "ANSI color codes", "ECMA-45"]
11+
license = {text = "Unlicense"}
12+
classifiers = [
13+
"Programming Language :: Python",
14+
"Programming Language :: Python :: 3",
15+
"Development Status :: 4 - Beta",
16+
"Environment :: Other Environment",
17+
"Intended Audience :: Developers",
18+
"License :: Public Domain",
19+
"Operating System :: OS Independent",
20+
"Topic :: Software Development :: Libraries :: Python Modules",
21+
]
22+
requires-python = ">=3.4"
23+
dependencies = [
24+
"RichConsole", # @ git+https://codeberg.org/KOLANICH-libs/RichConsole.py.git"
25+
]
26+
dynamic = ["version"]
27+
28+
[project.urls]
29+
Homepage = "https://codeberg.org/KOLANICH-libs/sideBySideDiff.py"
30+
31+
[tool.setuptools]
32+
zip-safe = true
33+
py-modules = ["sideBySideDiff"]
34+
35+
[tool.setuptools_scm]

sideBySideDiff.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import typing
2+
from math import log10, ceil
3+
import difflib
4+
from RichConsole import RichStr, groups, rsjoin
5+
6+
7+
def getMaxWidth(column: typing.List[str]) -> int:
8+
return max(len(str(e)) for e in column)
9+
10+
11+
def deletedStyle(l: str) -> RichStr:
12+
return groups.CrossedOut.crossedOut(groups.Fore.green(l))
13+
14+
15+
StyleT = typing.Callable[[str], typing.Union[str, RichStr]]
16+
StylePairT = typing.Tuple[StyleT, StyleT]
17+
18+
StringConverterT = typing.Callable[[typing.Any], str]
19+
20+
def dummy(x):
21+
return x
22+
23+
StyleDictT = typing.Dict[str, typing.Union[StyleT, StylePairT]]
24+
defaultStyle = {
25+
"equal": (dummy, dummy),
26+
"replace": (groups.Fore.lightblueEx, groups.Fore.lightblueEx),
27+
"insert": (groups.Back.lightyellowEx, groups.Fore.lightgreenEx),
28+
"delete": (deletedStyle, groups.Back.lightredEx),
29+
"lineNo": groups.Fore.lightcyanEx
30+
}
31+
32+
rangeOrSlice = typing.Union[slice, range]
33+
34+
35+
def azip(a: typing.Any, ar: rangeOrSlice, b: typing.Any, br: rangeOrSlice, substitute: typing.Any = None) -> typing.Iterator[typing.Any]:
36+
#ar.stop = typing.cast(int, ar.stop)
37+
#ar.start = typing.cast(int, ar.start)
38+
#br.stop = typing.cast(int, br.stop)
39+
#br.start = typing.cast(int, br.start)
40+
41+
al = ar.stop - ar.start
42+
bl = br.stop - br.start
43+
44+
d = al - bl
45+
comm = min(al, bl)
46+
yield from zip(a[ar.start : ar.start + comm], range(ar.start, ar.start + comm), b[br.start : br.start + comm], range(br.start, br.start + comm))
47+
48+
if d > 0:
49+
yield from zip(a[ar.start + comm : ar.stop], range(ar.start + comm, ar.stop), [substitute] * d, [None] * d)
50+
elif d < 0:
51+
yield from zip([substitute] * (-d), [None] * (-d), b[br.start + comm : br.stop], range(br.start + comm, br.stop))
52+
53+
54+
def genPadding(l: int, padder: str = " ") -> str:
55+
return padder * l
56+
57+
58+
LineNumberT = typing.Union[str, int]
59+
60+
61+
def genNum(n: LineNumberT, numWidth: int) -> str:
62+
r = str(n)
63+
return genPadding(numWidth - len(r), "0") + r
64+
65+
66+
def generateRichStrLineDiff(al: str, aln: LineNumberT, bl: str, bln: LineNumberT, opStyle: StylePairT, lineNoStyle: StyleT, maxW: typing.Tuple[int, int], numWidth: int, strConverter: StringConverterT) -> RichStr:
67+
if al is None:
68+
al = ""
69+
if bl is None:
70+
bl = ""
71+
al = strConverter(al)
72+
bl = strConverter(bl)
73+
74+
if aln is None:
75+
aln = genPadding(numWidth)
76+
77+
if bln is None:
78+
bln = genPadding(numWidth)
79+
80+
return rsjoin(" ", (
81+
lineNoStyle(genNum(aln, numWidth)),
82+
opStyle[0](al),
83+
genPadding(maxW[0] - len(al)),
84+
lineNoStyle(genNum(bln, numWidth)),
85+
opStyle[1](bl)
86+
))
87+
88+
89+
OpcodeT = typing.Tuple[str, int, int, int, int]
90+
91+
def sideBySideDiff(a: typing.List[str], b: typing.List[str], *, style: typing.Optional[StyleDictT] = None, strConverter: typing.Optional[StringConverterT] = None, names: typing.Optional[typing.Tuple[str, str]] = None, matcherClass: typing.Optional[typing.Type[difflib.SequenceMatcher]]=None, opcodes: typing.Optional[typing.Iterable[OpcodeT]]=None) -> typing.Iterator[RichStr]:
92+
style_ = type(defaultStyle)(defaultStyle)
93+
if style is not None:
94+
style_.update(style)
95+
if strConverter is None:
96+
strConverter = str
97+
98+
lns = style_["lineNo"]
99+
100+
101+
if opcodes is None:
102+
if matcherClass is None:
103+
matcherClass = difflib.SequenceMatcher
104+
m = matcherClass(None, a, b)
105+
opcodes = m.get_opcodes()
106+
107+
maxW = (getMaxWidth(a), getMaxWidth(b))
108+
numMaxWidth = ceil(log10(max(len(a), len(b))))
109+
110+
if names is not None:
111+
maxW = typing.cast(typing.Tuple[int, int], tuple(max(szs) for szs in zip(maxW, (len(n) for n in names))))
112+
secondHeaderPadding = 1 + numMaxWidth + maxW[0] # space between first line and its number # padding
113+
yield " ".join((genPadding(numMaxWidth), names[0], genPadding(secondHeaderPadding - len(names[0])), names[1]))
114+
115+
for oc, aS, aE, bS, bE in opcodes:
116+
opStyle = style_[oc]
117+
if oc == "equal":
118+
for al, aln, bl, bln in zip(a[aS:aE], range(aS, aE), b[bS:bE], range(bS, bE)):
119+
yield generateRichStrLineDiff(al, aln, bl, bln, opStyle, lns, maxW, numMaxWidth, strConverter)
120+
121+
elif oc == "replace":
122+
for al, aln, bl, bln in azip(a, range(aS, aE), b, range(bS, bE), None):
123+
yield generateRichStrLineDiff(al, aln, bl, bln, opStyle, lns, maxW, numMaxWidth, strConverter)
124+
elif oc == "insert":
125+
for bl, bln in zip(b[bS:bE], range(bS, bE)):
126+
yield generateRichStrLineDiff(genPadding(maxW[0]), genPadding(numMaxWidth), bl, bln, opStyle, lns, maxW, numMaxWidth, strConverter)
127+
elif oc == "delete":
128+
for al, aln in zip(a[aS:aE], range(aS, aE)):
129+
yield generateRichStrLineDiff(al, aln, genPadding(maxW[1]), genPadding(numMaxWidth), opStyle, lns, maxW, numMaxWidth, strConverter)
130+
131+
132+
if __name__ == "__main__":
133+
from pathlib import Path
134+
import sys
135+
with Path(sys.argv[1]).open("rt", encoding="utf-8") as f0:
136+
with Path(sys.argv[2]).open("rt", encoding="utf-8") as f1:
137+
for l in sideBySideDiff(f0.read().splitlines(), f1.read().splitlines()):
138+
print(l)

0 commit comments

Comments
 (0)