Skip to content

Instantly share code, notes, and snippets.

@greyblue9
Created December 19, 2021 09:59
Show Gist options
  • Save greyblue9/28edff746977433aad4d1e96a20a0f7f to your computer and use it in GitHub Desktop.
Save greyblue9/28edff746977433aad4d1e96a20a0f7f to your computer and use it in GitHub Desktop.
JSON to Python classes converter
#!/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