Skip to content

Commit 95794c6

Browse files
authored
fix: support SAS module help in PROC PYTHON (#1305)
1 parent 65e6b02 commit 95794c6

File tree

3 files changed

+219
-1
lines changed

3 files changed

+219
-1
lines changed

server/src/python/sas/sas2py.pyi

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
class SAS2py:
5+
"""
6+
This module provides the interface from the Python process to Proc Python in
7+
the SAS process. It provides user callable methods for interacting with the
8+
SAS process it's attached to.
9+
"""
10+
11+
workpath: str
12+
"""string containing the WORK libref's filesystem path, including the trailing slash."""
13+
14+
def hideLOG(self, tf: bool) -> None:
15+
"""
16+
This methods identifies whether the SAS LOG output for the data transfer methods, sd2df()
17+
and df2sd() is written or cached to a file. Since it's the Python log, and there's a lot
18+
of SAS code and messages that those methods generate, the default is to not see all
19+
of that cluttering up the Python log you're seeing. But, for diagnosing problems, having
20+
that shown can be helpful. When set to True (default), the contents of the SAS LOG for
21+
those methods is cached, and you can see it at any time using the printLOG() method, or
22+
clear the current contents of the cache file using clearLOG().
23+
24+
:param tf: boolean default True. Whether to hide the LOG output for data transfer routines
25+
26+
:return: None
27+
"""
28+
29+
def printLOG(self, method='SAS') -> None:
30+
"""
31+
This methods renders the parts of the SAS LOG output that were hidden from the data
32+
transfer routines sd2df() and df2sd(), based upon the setting of hideLOG() which
33+
defaults to True.
34+
35+
:param method: the default value 'SAS' uses SAS to write the output to the SAS LOG which allows
36+
for proper coloring of the output in Studio and also shows up before the Python
37+
output when in 'submit' processing.
38+
The value 'Python' uses Python to write the output to the Python log, which
39+
shows up in the SAS LOG without coloring and in the Python output where the method
40+
was executed, regardless of using 'submit' or 'interactive'.
41+
42+
43+
:return: None
44+
"""
45+
46+
def clearLOG(self) -> None:
47+
"""
48+
This method will delete all of the currently cached LOG output, if any. Subsequent output
49+
from data transfer methods will continue to cache their output, if set to hide that output,
50+
based upon the setting of hideLOG()
51+
52+
:return: None
53+
"""
54+
55+
def submit(self, code: str) -> int:
56+
"""
57+
This methods submits the code you provide back into the existing SAS session, recursively
58+
executing it while still within the PROC PYTHON that is running this method.
59+
60+
:param code: string of SAS code to submit
61+
62+
:return: None
63+
"""
64+
65+
def symget(self, name: str) -> str:
66+
"""
67+
This methods retrieves the value for the macro variable who's name you provided. It returns
68+
the string value. If the value represents a numeric type, you can simply cast it to what type
69+
you like.
70+
71+
:param name: string of SAS macro variable name
72+
73+
:return: str
74+
"""
75+
76+
def symput(self, name: str, val: str) -> int:
77+
"""
78+
This methods assigns a macro variable in SAS with the name and value you provide.
79+
80+
:param name: string of SAS macro variable name
81+
:param val: value to assign, which will be converted to a string, as that's what macro
82+
variable in SAS are
83+
84+
:return: int
85+
"""
86+
87+
def pyplot(self, plot: object, filename: str = None, filepath: str = None,
88+
filetype: str='svg', **kwargs) -> None:
89+
"""
90+
This methods renders a matplot.pyplot object, or other plot object that supports pyplot's
91+
savefig() method. It simply calls savefig() writing the plot to one of the supported ODS types
92+
and submits the SAS code to render that file using ODS.
93+
94+
:param plot: the plot object; pyplot or equivalent supporting the same savefig() method
95+
:param filename: name of the file to create, defaults to 'matplot'
96+
:param filepath: directory path to write the file; defaults to the work library
97+
:param filetype: file type to create; defaults to 'svg'. This is passed to savefig via format=filetype
98+
:param kwargs: kwargs passed to the savefig() method
99+
100+
:return: None
101+
"""
102+
103+
def renderImage(self, filename: str) -> None:
104+
"""
105+
This method renders a plot that has already been written to a file, so you
106+
can render plots from any plotting object that isn't pyplot or doesn't have the same
107+
savefig() method as pyplot. You write the plot to a supported ODS file type using that
108+
objects methods and just call this method to have it rendered.
109+
110+
:param filename: fully qualified name of the file to render.
111+
112+
:return: None
113+
"""
114+
115+
def logMessage(self, message: str, messageType: str = 'NOTE') -> None:
116+
"""
117+
Writes a well formed message to the SAS Log
118+
119+
:param message: {String} - Message that should be written to the SAS log
120+
:param messageType: {String - default: NOTE}
121+
- NOTE, writes a Note to the SAS log
122+
- WARNING, writes a Warning to the SAS log
123+
- ERROR, writes an Error to the SAS log
124+
125+
:return: None
126+
127+
# Example usage
128+
SAS.logMessage('test')
129+
SAS.logMessage('testWarn', 'warning')
130+
SAS.logMessage('testError', 'error')
131+
132+
"""
133+
134+
def sasdata2dataframe(self, dataset: str, rowsep: str = '\x01', colsep: str = '\x02',
135+
rowrep: str = ' ', colrep: str = ' ', **kwargs):
136+
"""
137+
See the doc for sd2df(). This is just an alias for that method
138+
"""
139+
140+
def sd2df(self, dataset: str, rowsep: str = '\x01', colsep: str = '\x02',
141+
rowrep: str = ' ', colrep: str = ' ', **kwargs):
142+
"""
143+
This method exports the SAS Data Set to a Pandas DataFrame, returning the DataFrame object.
144+
145+
:param dataset: the 'libref.table(optional dataset options)' name of the SAS Data Set
146+
147+
These parameters are not to be used normally. Don't use them without instruction.
148+
149+
:param rowsep: the row separator character to use; defaults to hex(1)
150+
:param colsep: the column separator character to use; defaults to hex(2)
151+
:param rowrep: the char to convert to for any embedded rowsep chars, defaults to ' '
152+
:param colrep: the char to convert to for any embedded colsep chars, defaults to ' '
153+
:param errors: this is the parameter to decode(errors=) when reading the stream of data into pandas and converting
154+
from bytes to chars. If the variables in the SAS data set have invalid characters (from truncation or other)
155+
then you can provide values like 'replace' or 'ignore' to load the invalid data instead of failing.
156+
:param kwargs: these are for internal use and are generally NOT needed.
157+
158+
:return: Pandas DataFrame
159+
"""
160+
161+
def dataframe2sasdata(self, df, dataset: str,
162+
LF: str = '\x01', CR: str = '\x02',
163+
colsep: str = '\x03', colrep: str = ' ',
164+
datetimes: dict={}, outfmts: dict={},
165+
labels: dict={}, char_lengths: dict={}, **kwargs):
166+
"""
167+
See the doc for df2sd(). This is just an alias for that method
168+
"""
169+
170+
def df2sd(self, df: 'pandas.DataFrame', dataset: str,
171+
LF: str = '\x01', CR: str = '\x02',
172+
colsep: str = '\x03', colrep: str = ' ',
173+
datetimes: dict={}, outfmts: dict={},
174+
labels: dict={}, char_lengths: dict={}, **kwargs) -> int:
175+
"""
176+
This method imports a Pandas DataFrame to a SAS Data Set you identify via the `dataset` parameter (libref.table).
177+
178+
Also note that DataFrame indexes (row label) are not transferred over as columns, as they aren't actually in df.columns.
179+
You can simply use df.reset_index() before this method and df.set_index() after to have the index be a column which
180+
is transferred over to the SAS data set. If you want to create a SAS index at the same time, specify that with the
181+
output dataset options.
182+
183+
:param df: Pandas DataFrame to import to a SAS Data Set
184+
:param dataset: the 'libref.table(optional output data set options)' name of the SAS Data Set to create
185+
:param datetimes: dict with column names as keys and values of 'date' or 'time' to create SAS date or times instead of datetimes
186+
:param outfmts: dict with column names and SAS formats to assign to the new SAS data set
187+
:param labels: dict with column names and labels to assign to the new SAS data set
188+
:param char_lengths: a dictionary containing the names:lengths of all of the character columns. This eliminates
189+
running the code to calculate the lengths, and goes straight to transferring the data
190+
191+
These parameters are not to be used normally. Don't use them without instruction.
192+
193+
:param LF: the character to use for LF when transferring the data; defaults to hex(1)
194+
:param CR: the character to use for CR when transferring the data; defaults to hex(2)
195+
:param colsep: the column separator character used for streaming the delimited data to SAS defaults to hex(3)
196+
:param colrep: the char to convert to for any embedded colsep, LF, CR chars in the data; defaults to ' '
197+
198+
:return: int
199+
"""
200+
201+
def sasfnc(self, *arg) -> str:
202+
"""
203+
This method executes the SAS or FCMP function you provide, returning the results.
204+
The parameters vary based upon the function being called. But the first parameter
205+
is the name of the function, followed by the required parameters for that function.
206+
207+
:param arg[1]: name of the SAS function to call
208+
:param arg[2] to arg[n]: arguments to SAS function
209+
210+
:return: str
211+
"""

server/src/python/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export const extractPythonCodes = (
1212
languageService: LanguageServiceProvider,
1313
): string => {
1414
const codeZoneManager = languageService.getCodeZoneManager();
15-
const pythonDocLines = [];
15+
const pythonDocLines = [
16+
"import sas2py #type: ignore",
17+
"SAS = sas2py.SAS2py()",
18+
];
1619
const symbols: DocumentSymbol[] = languageService.getDocumentSymbols();
1720
for (let i = 0; i < symbols.length; i++) {
1821
const symbol = symbols[i];

tools/build.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ if (process.env.npm_config_webviews || process.env.npm_config_client) {
9191
src: "./server/node_modules/pyright-internal-node/dist/packages/pyright-internal/typeshed-fallback",
9292
dest: "./server/dist/node/typeshed-fallback",
9393
},
94+
{
95+
src: "./server/src/python/sas",
96+
dest: "./server/dist/node/typeshed-fallback/stubs/sas",
97+
},
9498
];
9599
for (const item of foldersToCopy) {
96100
fs.cpSync(item.src, item.dest, { recursive: true });

0 commit comments

Comments
 (0)