Skip to content

Commit e200f83

Browse files
new function setupPythonEnv
1 parent 75d6944 commit e200f83

1 file changed

Lines changed: 390 additions & 0 deletions

File tree

toolbox/utilities/setupPythonEnv.m

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
function setupPythonEnv(varargin)
2+
%setupPythonEnv configures a Python environment compatible with MATLAB.
3+
%
4+
%<a href="matlab: docsearchFS('setupPythonEnv')">Link to the help function</a>
5+
%
6+
% This function detects a Python installation compatible with the current
7+
% MATLAB release, excludes unsupported system interpreters (such as the
8+
% Python distributed with Xcode on macOS), configures pyenv accordingly,
9+
% and optionally executes a pip command in the selected Python environment.
10+
%
11+
% Required input arguments:
12+
%
13+
% None.
14+
%
15+
% Optional input arguments:
16+
%
17+
% PipCommand : Pip command to be executed in the selected Python
18+
% environment. If PipCommand does not start with a standard
19+
% pip action such as 'install', 'uninstall', 'list' or
20+
% 'show', and contains a single token only, it is
21+
% interpreted as a package name and automatically expanded
22+
% to 'install <package>'.
23+
% Example - 'PipCommand','install yfinance'
24+
% Example - 'PipCommand','yfinance'
25+
% Data Types - char | string
26+
%
27+
% Verbose : Logical scalar which controls the display of diagnostic
28+
% messages.
29+
% Default is true.
30+
% Example - 'Verbose',false
31+
% Data Types - logical
32+
%
33+
% Output:
34+
%
35+
% This function does not return output arguments. It configures the
36+
% Python environment for the current MATLAB session and optionally
37+
% executes a pip command.
38+
%
39+
% See also: pyenv, pyrun, system
40+
%
41+
% Copyright 2008-2026.
42+
% Written by FSDA team
43+
%
44+
%<a href="matlab: docsearchFS('setupPythonEnv')">Link to the help page for this function</a>
45+
%
46+
%$LastChangedDate:: $: Date of the last commit
47+
%
48+
49+
% Examples:
50+
%
51+
%{
52+
% Just configure a compatible Python environment.
53+
setupPythonEnv
54+
%}
55+
56+
%{
57+
% Install Python package yfinance in the selected environment.
58+
setupPythonEnv('PipCommand','install yfinance');
59+
%}
60+
61+
%{
62+
% Equivalent compact syntax: if a single package name is supplied, it
63+
% is interpreted as an install request.
64+
setupPythonEnv('PipCommand','yfinance');
65+
%}
66+
67+
%{
68+
% Show the list of installed packages.
69+
setupPythonEnv('PipCommand','list');
70+
%}
71+
72+
%{
73+
% Show reduced output.
74+
setupPythonEnv('PipCommand','show yfinance','Verbose',false);
75+
%}
76+
77+
%% Beginning of code
78+
79+
PipCommand = '';
80+
Verbose = true;
81+
82+
if nargin > 0
83+
84+
options = struct('PipCommand',PipCommand,'Verbose',Verbose);
85+
[varargin{:}] = convertStringsToChars(varargin{:});
86+
UserOptions = varargin(1:2:length(varargin));
87+
88+
if ~isempty(UserOptions)
89+
if length(varargin) ~= 2*length(UserOptions)
90+
error('FSDA:setupPythonEnv:WrongInputOpt', ...
91+
['Number of supplied options is invalid. Probably values ' ...
92+
'for some parameters are missing.']);
93+
end
94+
aux.chkoptions(options,UserOptions);
95+
end
96+
97+
for i = 1:2:length(varargin)
98+
options.(varargin{i}) = varargin{i+1};
99+
end
100+
101+
PipCommand = options.PipCommand;
102+
Verbose = options.Verbose;
103+
end
104+
105+
% 1. Check operating system, detect a compatible Python interpreter and
106+
% configure pyenv if needed.
107+
try
108+
[setup_success, python_exe] = run_system_checks(Verbose);
109+
catch ME
110+
error('FSDA:setupPythonEnv:SystemCheckError', ...
111+
'Critical error during system analysis: %s', ME.message);
112+
end
113+
114+
if ~setup_success
115+
if Verbose
116+
disp('Operation aborted. Python requirements not met.');
117+
end
118+
return;
119+
end
120+
121+
% 2. Optionally execute a pip command in the selected environment.
122+
if ~isempty(PipCommand)
123+
124+
PipCommand = strtrim(char(PipCommand));
125+
pipCmdPadded = [PipCommand ' '];
126+
127+
isExplicitCmd = startsWith(pipCmdPadded,'install ') || ...
128+
startsWith(pipCmdPadded,'uninstall ') || ...
129+
startsWith(pipCmdPadded,'list ') || ...
130+
startsWith(pipCmdPadded,'show ') || ...
131+
strcmp(PipCommand,'list') || ...
132+
startsWith(PipCommand,'--version');
133+
134+
if ~isExplicitCmd && ~contains(PipCommand,' ')
135+
full_command = ['install ' PipCommand];
136+
else
137+
full_command = PipCommand;
138+
end
139+
140+
sys_cmd = ['"' char(python_exe) '" -m pip ' full_command];
141+
142+
if Verbose
143+
disp(['Internal pip execution: ' sys_cmd]);
144+
end
145+
146+
[status, cmdout] = system(sys_cmd);
147+
148+
if status == 0
149+
if Verbose && ~isempty(strtrim(cmdout))
150+
disp(cmdout);
151+
end
152+
else
153+
error('FSDA:setupPythonEnv:PipExecutionError', ...
154+
'Error during pip execution:\n%s', cmdout);
155+
end
156+
end
157+
158+
end
159+
160+
% -------------------------------------------------------------------------
161+
% Subfunctions
162+
% -------------------------------------------------------------------------
163+
164+
function [success, valid_path] = run_system_checks(Verbose)
165+
166+
success = false;
167+
valid_path = "";
168+
169+
% Extract current MATLAB release, for example '2024b'.
170+
m_rel = version('-release');
171+
pe = pyenv;
172+
173+
% Anonymous function to identify Python interpreters associated with Xcode.
174+
is_xcode = @(p) contains(p,'Xcode.app') || strcmp(p,'/usr/bin/python3');
175+
176+
% If pyenv is already loaded, reuse it unless it points to the Xcode
177+
% Python interpreter on macOS.
178+
if pe.Executable ~= "" && pe.Status == "Loaded"
179+
if ismac && is_xcode(char(pe.Executable))
180+
error(['MATLAB has already loaded the Xcode Python interpreter. ' ...
181+
'Restart MATLAB to apply the bypass.']);
182+
else
183+
success = true;
184+
valid_path = char(pe.Executable);
185+
return;
186+
end
187+
end
188+
189+
if Verbose
190+
disp('--- Starting Python Environment Analysis ---');
191+
end
192+
193+
if ispc
194+
195+
if Verbose
196+
disp('Operating System: Windows');
197+
end
198+
199+
paths = get_system_paths('windows');
200+
[valid_path, python_exists] = find_compatible_python(paths,m_rel,false);
201+
202+
if ~python_exists
203+
if Verbose
204+
disp('-> No Python installation detected on the system.');
205+
suggest_installation(m_rel);
206+
end
207+
elseif valid_path == ""
208+
if Verbose
209+
disp(['-> Python is installed, but the version is not ' ...
210+
'compatible with the MATLAB release.']);
211+
suggest_installation(m_rel);
212+
end
213+
else
214+
success = true;
215+
end
216+
217+
elseif ismac
218+
219+
if Verbose
220+
disp('Operating System: macOS');
221+
end
222+
223+
[xcode_status, ~] = system('xcode-select -p 2>/dev/null');
224+
xcode_installed = (xcode_status == 0);
225+
paths = get_system_paths('macos');
226+
227+
if ~xcode_installed
228+
229+
if Verbose
230+
disp('Xcode Status: Not installed.');
231+
end
232+
233+
[valid_path, python_exists] = find_compatible_python(paths,m_rel,false);
234+
235+
if ~python_exists
236+
if Verbose
237+
disp('-> Python Detection: No installation found.');
238+
suggest_installation(m_rel);
239+
end
240+
elseif valid_path == ""
241+
if Verbose
242+
disp('-> Python Detection: Versions found but not compatible with MATLAB.');
243+
suggest_installation(m_rel);
244+
end
245+
else
246+
success = true;
247+
end
248+
249+
else
250+
251+
if Verbose
252+
disp('Xcode Status: Installed (activating system dependencies bypass).');
253+
end
254+
255+
[valid_path, python_exists] = find_compatible_python(paths,m_rel,true);
256+
257+
if ~python_exists
258+
if Verbose
259+
disp('-> Python Detection: No valid non-Xcode installation found.');
260+
suggest_installation(m_rel);
261+
end
262+
elseif valid_path == ""
263+
if Verbose
264+
disp('-> Python Detection: Independent installations are not compatible.');
265+
suggest_installation(m_rel);
266+
end
267+
else
268+
success = true;
269+
end
270+
end
271+
272+
else
273+
error('FSDA:setupPythonEnv:UnsupportedOS', ...
274+
'Operating system not supported.');
275+
end
276+
277+
if success && pe.Executable == ""
278+
pyenv('Version',valid_path);
279+
if Verbose
280+
disp(['-> Configuration completed. Executable in use: ' valid_path]);
281+
end
282+
end
283+
284+
end
285+
286+
function paths = get_system_paths(os_type)
287+
288+
if strcmp(os_type,'windows')
289+
[~, cmdout] = system('where python python3 python3.13 2>nul');
290+
fallback = "";
291+
else
292+
[~, cmdout] = system('/bin/zsh -lc "which -a python3 python3.13 2>/dev/null"');
293+
fallback = sprintf(['\n/opt/homebrew/bin/python3' ...
294+
'\n/opt/homebrew/bin/python3.13' ...
295+
'\n/usr/local/bin/python3' ...
296+
'\n/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13']);
297+
end
298+
299+
raw_string = string(cmdout) + string(fallback);
300+
paths = unique(split(raw_string,newline),'stable');
301+
302+
end
303+
304+
function [valid_path, any_python_exists] = find_compatible_python(paths,m_rel,bypass_xcode)
305+
306+
valid_path = "";
307+
any_python_exists = false;
308+
309+
for i = 1:length(paths)
310+
311+
curr_str = strtrim(paths(i));
312+
313+
if curr_str == "" || ~isfile(curr_str)
314+
continue
315+
end
316+
317+
curr_char = char(curr_str);
318+
319+
if bypass_xcode && ...
320+
(contains(curr_char,'Xcode.app') || strcmp(curr_char,'/usr/bin/python3'))
321+
continue
322+
end
323+
324+
[status, ver_str] = system(['"' curr_char '" --version']);
325+
326+
if status == 0
327+
any_python_exists = true;
328+
if check_version_compatibility(ver_str,m_rel)
329+
valid_path = curr_char;
330+
break
331+
end
332+
end
333+
end
334+
335+
end
336+
337+
function is_compat = check_version_compatibility(ver_str,m_rel)
338+
339+
tokens = regexp(string(ver_str),'3\.\d+','match');
340+
341+
if isempty(tokens)
342+
is_compat = false;
343+
return
344+
end
345+
346+
py_ver_short = tokens(1);
347+
348+
switch m_rel
349+
case {'2026a','2025b'}
350+
compat_list = ["3.10","3.11","3.12","3.13"];
351+
case {'2025a','2024b'}
352+
compat_list = ["3.9","3.10","3.11","3.12"];
353+
case {'2024a','2023b'}
354+
compat_list = ["3.9","3.10","3.11"];
355+
case {'2023a','2022b'}
356+
compat_list = ["3.8","3.9","3.10"];
357+
case {'2022a','2021b'}
358+
compat_list = ["3.7","3.8","3.9"];
359+
otherwise
360+
compat_list = [];
361+
end
362+
363+
is_compat = ismember(py_ver_short,compat_list);
364+
365+
end
366+
367+
function suggest_installation(m_rel)
368+
369+
disp('---------------------------------------------------------');
370+
disp(['ACTION REQUIRED: Install a Python version compatible with MATLAB R' m_rel]);
371+
disp('Download: https://www.python.org/downloads/');
372+
373+
switch m_rel
374+
case {'2026a','2025b'}
375+
disp('Supported: 3.10, 3.11, 3.12, 3.13');
376+
case {'2025a','2024b'}
377+
disp('Supported: 3.9, 3.10, 3.11, 3.12');
378+
case {'2024a','2023b'}
379+
disp('Supported: 3.9, 3.10, 3.11');
380+
case {'2023a','2022b'}
381+
disp('Supported: 3.8, 3.9, 3.10');
382+
case {'2022a','2021b'}
383+
disp('Supported: 3.7, 3.8, 3.9');
384+
otherwise
385+
disp('MATLAB version not classified within the R2021b-R2026a range.');
386+
end
387+
388+
disp('---------------------------------------------------------');
389+
390+
end

0 commit comments

Comments
 (0)