Created
December 19, 2021 09:59
-
-
Save greyblue9/28edff746977433aad4d1e96a20a0f7f to your computer and use it in GitHub Desktop.
JSON to Python classes converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Modified 2021-12-19 by @greyblue92 | |
# - Convert from python 2 to python 3 | |
# - Add API call to convert json to C# | |
# - Fix a bunch of problems with the python output | |
# - Type renaming and generics | |
# Original code from https://github.com/shannoncruey/csharp-to-python | |
######################################################################## | |
# Apache License, Version 2.0 (the "License"); | |
# the License. | |
# obtain a copy of the License at | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# in writing, software | |
# an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# permissions and | |
# the License. | |
######################################################################## | |
""" | |
# JSON to Python classes | |
# (formerly C#.NET to Python) | |
5-1-2012 NSC | |
Use is simple: | |
2) run this script with a file or pass your JSON via stdin | |
3) the results of the conversion will be written to stdout | |
""" | |
import re | |
import os | |
import json | |
import sys | |
from pathlib import Path | |
import textwrap | |
import urllib3 | |
out = "" | |
text = "" | |
for arg in reversed(sys.argv[1:]): | |
if arg and os.path.exists(arg): | |
infile = Path(Path(arg).absolute()) | |
text = infile.read_text() | |
if not text: | |
while True: | |
ln = sys.stdin.readline() | |
if not ln: | |
break | |
ln = ln.strip("\r\n") | |
text += ("\x0a" if text else "") + ln | |
url = "https://json2csharp.com/api/Default" | |
scheme, host, _ = urllib3.get_host(url) | |
pool = ( | |
urllib3.HTTPSConnectionPool(host) | |
if scheme == "https" else | |
urllib3.HTTPConnectionPool(host) | |
) | |
body_json = json.dumps( | |
{ | |
"input": text, | |
"operationid":"jsontocsharp", | |
"settings":{ | |
"UsePascalCase":False, | |
"UseJsonAttributes":False, | |
"UseFields":True, | |
"UseJsonPropertyName":False, | |
"ImmutableClasses":True, | |
"NoSettersForCollections":True, | |
} | |
} | |
); | |
cstext = "" | |
response = pool.urlopen( | |
method="POST", | |
url=url, | |
body=body_json, | |
headers={ | |
"accept": "*/*", | |
"accept-language": "en-US,en-IN;q=0.9,en;q=0.8", | |
"content-type": "application/json; charset=utf-8", | |
"sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="96"', | |
"sec-ch-ua-mobile": "?1", | |
"sec-ch-ua-platform": '"Android"', | |
"sec-fetch-dest": "empty", | |
"sec-fetch-mode": "cors", | |
"sec-fetch-site": "same-origin", | |
"x-requested-with": "XMLHttpRequest", | |
}, | |
retries=3, | |
redirect=True, | |
assert_same_host=False, | |
timeout=30, | |
pool_timeout=None, | |
release_conn=None, | |
chunked=False, | |
body_pos=None, | |
) | |
if response.status == 200: | |
data = response.data.decode("utf-8") | |
if ( | |
(data.startswith('"') or data.startswith("'")) | |
and data[0] == data[-1] | |
): | |
import ast | |
data = ast.literal_eval(data) | |
if data.startswith("Exception: "): | |
errmsg = data.partition(": ")[2] | |
raise ValueError(errmsg, iter([text])) | |
cstext = data | |
cur_class = "" | |
types = { | |
"string": "str", | |
"String": "str", | |
"Integer": "int", | |
"Int32": "int", | |
"long": "int", | |
"short": "int", | |
"double": "float", | |
"decimal": "decimal.decimal", | |
"Decimal": "decimal.decimal", | |
"Uri": "URI", | |
"Url": "URL", | |
"FileInfo": "Path", | |
"DirectoryInfo": "Path", | |
"ReadOnlyList": "list", | |
"IEnumerable": "Iterable", | |
"Enumerable": "Iterable", | |
"Enumerator": "Iterator", | |
} | |
if cstext: | |
cstext = textwrap.dedent( | |
"\x0a".join( | |
l for l in | |
cstext.replace("\r\n", "\n").replace("\r", "\n").splitlines() | |
if not l.strip().startswith("//") | |
and not l.strip().startswith("[") | |
).replace("\n{", " {") | |
) | |
cstext = textwrap.dedent(cstext) | |
lines = cstext.splitlines() | |
for idx, line in enumerate(lines): | |
# FIRST THINGS FIRST ... for Python, we wanna fix tabs into spaces... | |
# we want all our output to have spaces instead of tabs | |
line = line.replace("\t", " ") | |
if True: | |
# This is the C# -> Python conversion | |
INDENT = 0 | |
sINDENTp4 = " " | |
# now that the tabs are fixed, | |
# we wanna count the whitespace at the beginning of the line | |
# might use it later for indent validation, etc. | |
p = re.compile(r"^(\s+)") | |
m = p.match(line) | |
if m: | |
INDENT = len(m.group()) | |
else: | |
INDENT = 4 | |
sINDENT = " " * INDENT | |
# this string global contains the current indent level + 4 | |
sINDENTp4 = " " * (INDENT+4) | |
# + 8 | |
sINDENTp8 = " " * (INDENT+8) | |
line = line.replace(" readonly ", " ") | |
# braces are a tricky, but lets first just remove any lines that only contain an open/close brace | |
if line.strip() == "{": continue # pass | |
if line.strip() == "}": continue # pass | |
# if the brace appears at the end of a line (like after an "if") | |
if len(line.strip()) > 1: | |
s = line.strip() | |
if s[-1] == "{": | |
line = s[:-1] | |
# comments | |
if line.strip()[:2] == "//": | |
line = line.replace("//", "# ") | |
line = line.replace("/*", "\"\"\"").replace("*/", "\"\"\"") | |
# some comments are at the end of a line | |
line = line.replace("; //", " # ") | |
# Fixing semicolon line endings (not otherwise, may be in a sql statement or something) | |
line = line.replace(";\n", "\n") | |
# Fixing line wrapped string concatenations | |
line = line.replace(") +\n", ") + \\\n") # lines that had an inline if still need the + | |
line = line.replace("+\n", "\\\n") | |
# Fixing function declarations... | |
line = line.replace(" public ", " ") | |
line = line.replace(" private ", " ") | |
if line.strip()[:3] == "def" and line.strip().endswith(")"): | |
line = line.replace(")", "):") | |
# Fixing variable declarations... | |
# doing "string" and "int" again down below with a regex, because they could be a line starter, or part of a bigger word | |
# line = line.replace(" int ", " ").replace(" string ", " ").replace(" bool ", " ").replace(" void ", " ") | |
# line = line.replace("(int ", "(").replace("(string ", "(").replace("(bool ", "(") | |
for cstype, pytype in types.items(): | |
line = re.subn( | |
f"(?<=[^a-zA-Z0-9_])({cstype})(?=$|[^a-zA-Z0-9_])", | |
pytype, | |
line | |
)[0] | |
# common C# functions and keywords | |
line = line.replace(".ToString()", "") # no equivalent, not necessary | |
line = line.replace(".ToLower()", ".lower()") | |
line = line.replace(".IndexOf(", ".find(") | |
line = line.replace(".Replace", ".replace") | |
line = line.replace(".Split", ".split") | |
line = line.replace(".Trim()", ".strip()") | |
line = line.replace("else if", "elif") | |
line = line.replace("!string.IsNullOrEmpty(", "") | |
line = line.replace("string.IsNullOrEmpty(", "not ") | |
line = line.replace("this.", "self.") | |
# Try/Catch blocks | |
line = line.replace("try", "try:") | |
line = line.replace("catch (Exception ex)", "except Exception:") | |
# I often threw "new exceptions" - python doesn't need the extra stuff | |
line = line.replace("new Exception", "Exception") | |
line = line.replace("throw", "raise") | |
# NULL testing | |
line = line.replace("== null", "is None") | |
line = line.replace("!= null", "is not None") | |
# ##### CUSTOM REPLACEMENTS ##### | |
line = line.replace("\" + Environment.NewLine", "\\n\"") | |
line = line.replace("HttpContext.Current.Server.MapPath(", "") # this will leave a trailing paren ! | |
line = line.replace(").Value", ", \"\") # WAS A .Value - confirm") #should work most of the time for Cato code | |
line = line.replace(".Length", ".__LENGTH") | |
# | |
# #these commonly appear on a line alone, and we aren't using them any more | |
if line.strip() == "dataAccess dc = new dataAccess()": pass | |
if line.strip() == "acUI.acUI ui = new acUI.acUI()": pass | |
if line.strip() == "sErr = \"\"": pass | |
if line.lower().strip() == "ssql = \"\"": pass # there's mixed case usage of "sSql" | |
if "dataAccess.acTransaction" in line: pass | |
if line.strip() == "DataRow dr = null": pass | |
if line.strip() == "DataTable dt = new DataTable()": pass | |
if "FunctionTemplates.HTMLTemplates ft" in line: pass | |
# | |
# | |
# # a whole bunch of common phrases from Cato C# code | |
line = line.replace("ui.", "uiCommon.") | |
line = line.replace("ft.", "ST.") # "FunctionTemplates ft is now import stepTemplates as sST" | |
line = line.replace("dc.IsTrue", "uiCommon.IsTrue") | |
line = line.replace("../images", "static/images") | |
# | |
# #99% of the time we won't want a None return, but an empty string instead | |
line = line.replace("return\n", "return \"\"\n") | |
# | |
# | |
# | |
# # this will *usually work | |
line = line.replace("DataRow dr in dt.Rows", "dr in dt") | |
line = line.replace("dt.Rows.Count > 0", "dt") | |
line = line.replace("Step oStep", "oStep") | |
line = line.replace("+ i +", "+ str(i) +") | |
line = line.replace("i+\\", "i += 1") | |
# # this will be helpful | |
# | |
# # true/false may be problematic, but these should be ok | |
line = line.replace(", true", ", True").replace(", false", ", False") | |
# | |
# ##### END CUSTOM ##### | |
# xml/Linq stuff | |
# the following lines were useful, but NOT foolproof, in converting some Linq XDocument/XElement stuff | |
# to Python's ElementTree module. | |
line = line.replace(".Attribute(", ".get(") | |
line = line.replace(".Element(", ".find(") | |
line = line.replace("XDocument.Load", "ET.parse") | |
line = line.replace("XDocument ", "").replace("XElement ", "") | |
line = line.replace("XDocument.Parse", "ET.fromstring") | |
line = line.replace("IEnumerable<XElement> ", "") | |
# note the order of the following | |
line = line.replace(".XPathSelectElements", ".findall").replace(".XPathSelectElement", ".find") | |
line = line.replace(".SetValue", ".text") | |
line = line.replace("ex.Message", "traceback.format_exc()") | |
# ##### CUSTOM ##### | |
# #!!! this has to be done after the database stuff, because they all use a "ref sErr" and we're matching on that! | |
# # passing arguments by "ref" doesn't work in python, mark that code obviously | |
# # because it need attention | |
line = line.replace("ref ", "0000BYREF_ARG0000") | |
# | |
# # the new way of doing exceptions - not raising them, appending them to an output object | |
line = line.replace("raise ex", "uiGlobals.request.Messages.append(traceback.format_exc())") | |
line = line.replace("raise Exception(", "uiGlobals.request.Messages.append(") | |
# | |
# | |
# # if this is a function declaration and it's a "wm" web method, | |
# # throw the new argument getter line on there | |
s = "" | |
p = re.compile("\(.*\)") | |
m = p.search(line) | |
"""if m: | |
args = m.group().replace("(","").replace(")","") | |
for arg in args.split(","): | |
s = s + sINDENTp4 + "%s = uiCommon.getAjaxArg(\"%s\")\n" % (arg.strip(), arg.strip()) | |
line = line.replace(args, "self") + s | |
# """ | |
##### END CUSTOM ##### | |
# else statements on their own line | |
if line.strip() == "else": | |
line = line.replace("else", "else:") | |
# let's try some stuff with regular expressions | |
# string and int declarations | |
p = re.compile("^int ") | |
m = p.match(line) | |
if m: | |
line = line.replace("int ", "") | |
p = re.compile("^string ") | |
m = p.match(line) | |
if m: | |
line = line.replace("string ", "") | |
# if statements | |
p = re.compile(".*if \(.*\)") | |
m = p.match(line) | |
if m: | |
line = line.replace("if (", "if ") | |
line = line[:-2] + ":\n" | |
line = line.replace("):", ":") | |
# foreach statements (also marking them because type declarations may need fixing) | |
p = re.compile(".*foreach \(.*\)") | |
m = p.match(line) | |
if m: | |
line = line.replace("foreach (", "for ") | |
line = line[:-2] + ":\n" | |
line = "### CHECK NEXT LINE for type declarations !!!\n" + line | |
p = re.compile(".*while \(.*\)") | |
m = p.match(line) | |
if m: | |
line = line.replace("while (", "while ") | |
line = line[:-2] + ":\n" | |
line = "### CHECK NEXT LINE for type declarations !!!\n" + line | |
# this is a crazy one. Trying to convert inline 'if' statements | |
#first, does it look like a C# inline if? | |
p = re.compile("\(.*\?.*:.*\)") | |
m = p.search(line) | |
if m: | |
pre_munge = m.group() | |
# ok, let's pick apart the pieces | |
p = re.compile("\(.*\?") | |
m = p.search(line) | |
if_part = m.group().replace("(", "").replace("?", "").replace(")", "").strip() | |
p = re.compile("\?.*:") | |
m = p.search(line) | |
then_part = m.group().replace(":", "").replace("?", "").strip() | |
p = re.compile(":.*\)") | |
m = p.search(line) | |
else_part = m.group().replace(":", "").replace("?", "").replace(")", "").strip() | |
#now reconstitute it (don't forget the rest of the line | |
post_munge = "(%s if %s else %s)" % (then_part, if_part, else_part) | |
line = line.replace(pre_munge, post_munge) | |
# && and || comparison operators in an "if" statement | |
p = re.compile("^.*if.*&&") | |
m = p.search(line) | |
if m: | |
line = line.replace("&&", "and") | |
line = "### VERIFY 'ANDs' !!!\n" + line | |
p = re.compile("^.*if.*\|\|") | |
m = p.search(line) | |
if m: | |
line = line.replace("||", "or") | |
line = "### VERIFY 'ORs' !!!\n" + line | |
line = line.replace("def class ", "class ") | |
if ( | |
line.strip().startswith("def ") | |
or line.strip().startswith("class ") | |
): | |
if not line.split("#")[0].strip().endswith(":"): | |
line = line.split("#")[0] + ":" + "#".join(line.split("#")[1:]) | |
# ALL DONE! | |
# e.g. "def requires_python;:" | |
m = re.search( | |
"^(?P<ws>\s*)(?P<def>def |)((?P<type>[a-zA-Z0-9_]+)(?P<generics><.*>|) |)(?P<id>[a-zA-Z0-9_]+);? *((?P<colon>:)|$|(?P<comma>,))", | |
line, re.DOTALL | |
) | |
if m: | |
gd = m.groupdict() | |
typepart = "" | |
if gd["type"]: | |
generics = "" | |
if gd["generics"]: | |
generics = gd["generics"] \ | |
.replace("<","[").replace(">","]") | |
typename = gd['type'] | |
if ( | |
len(typename) > 2 | |
and typename[1].isupper() | |
and typename[0] == "I" | |
): | |
typename = typename[1:].lower() | |
if typename.lower() in __import__("builtins").__dict__: | |
typename = typename.lower() | |
typepart = f": {typename}{generics}" | |
if typepart == ": class": | |
line = f"{gd['ws']}class {gd['id']}:" | |
cur_class = gd['id'] | |
else: | |
line = f"{gd['ws']}{gd['id']}{typepart}" | |
if gd['comma']: | |
line += "," | |
if ( | |
cur_class and | |
( | |
line.strip().startswith("def " + cur_class + "(") | |
or line.strip() == cur_class + "(" | |
) | |
): | |
lx = line.strip() | |
ws = line[0:len(line)-len(lx)] | |
line = f"{ws}def __init__(" | |
if line.strip() == ")": | |
line += ":" | |
if line.strip() == "def": | |
continue | |
while True: | |
pts = line.strip().strip(":").strip().split() | |
if len(pts) > 1 and pts[-2] == "class": | |
lx = line.strip() | |
ws = line[0:len(line)-len(lx)] | |
cur_class = pts[-1] | |
line = f"{ws}class {cur_class}:" | |
nidx = idx + 1 | |
while not lines[nidx].strip(): | |
nidx += 1 | |
lines[nidx] = re.sub( | |
"^(\\s*)(?!def )([^ ]+)", | |
"\\1def \\2", | |
lines[nidx] | |
) | |
m = re.search( | |
"\\b(?P<id>[a-zA-Z0-9_]+)<(?P<generic>[^<>]+)>\\b", | |
line | |
) | |
if not m: | |
break | |
typename = m.group("id") | |
if ( | |
len(typename) > 2 | |
and typename[1].isupper() | |
and typename[0] == "I" | |
): | |
typename = typename[1:].lower() | |
if typename.lower() in __import__("builtins").__dict__: | |
typename = typename.lower() | |
line = line.replace( | |
m.group("id") + "<" + m.group("generic") + ">", | |
( | |
typename + | |
"[" + | |
m.group("generic") + | |
"]" | |
) | |
) | |
out += line + "\x0a" | |
print(out) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment