Skip to content

Commit 0d7490c

Browse files
committed
[IMP] add evaluation for request.env
- add evaluation for odoo.http.request: - up to 15.2: WebRequest class - since 15.3: Request class - add env variable to Request class (15.3+ only, as it already exists as property before that) - add evaluation to (Request/WebRequest).env: Environment | None This allows for better type evaluation in controllers, which use odoo.http.request extensively.
1 parent 559413b commit 0d7490c

File tree

6 files changed

+225
-4
lines changed

6 files changed

+225
-4
lines changed

server/src/core/python_arch_builder_hooks.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,28 @@ static arch_class_hooks: Lazy<Vec<PythonArchClassHook>> = Lazy::new(|| {vec![
3838
let mut range = symbol.borrow().range().clone();
3939
let slots = symbol.borrow().get_symbol(&(vec![], vec![Sy!("__slots__")]), u32::MAX);
4040
if slots.len() == 1 {
41-
if slots.len() == 1 {
42-
range = slots[0].borrow().range().clone();
43-
}
41+
range = slots[0].borrow().range().clone();
4442
}
4543
symbol.borrow_mut().add_new_variable(session, Sy!("env"), &range);
4644
}
4745
}
4846
},
47+
PythonArchClassHook {
48+
odoo_entry: true,
49+
trees: vec![
50+
(Sy!("15.3"), Sy!("19.2"), (vec![Sy!("odoo"), Sy!("http")], vec![Sy!("Request")])),
51+
(Sy!("19.2"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("http"), Sy!("requestlib")], vec![Sy!("Request")]))
52+
],
53+
func: |session: &mut SessionInfo, _entry_point: &Rc<RefCell<EntryPoint>>, symbol: Rc<RefCell<Symbol>>| {
54+
// ----------- Request.env ------------
55+
let has_env = !symbol.borrow().get_content_symbol(&Sy!("env"), u32::MAX).symbols.is_empty();
56+
if has_env {
57+
return;
58+
}
59+
let range = symbol.borrow().range().clone();
60+
symbol.borrow_mut().add_new_variable(session, Sy!("env"), &range);
61+
}
62+
},
4963
PythonArchClassHook {
5064
odoo_entry: true,
5165
trees: vec![

server/src/core/python_arch_eval_hooks.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,58 @@ static arch_eval_file_hooks: Lazy<Vec<PythonArchEvalFileHook>> = Lazy::new(|| {v
6565
env.set_doc_string(Some(S!("")));
6666
}
6767
}},
68+
PythonArchEvalFileHook {odoo_entry: true,
69+
trees: vec![(Sy!("0.0"), Sy!("15.3"), (vec![Sy!("odoo"), Sy!("http")], vec![Sy!("request")]))],
70+
if_exist_only: true,
71+
func: |_odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
72+
// --------- request: WebRequest (before 15.3) ---------
73+
let web_request_class_syms = file_symbol.borrow().get_symbol(&(vec![], vec![Sy!("WebRequest")]), u32::MAX);
74+
let Some(web_request_class) = web_request_class_syms.last() else {
75+
return;
76+
};
77+
let mut request = symbol.borrow_mut();
78+
request.set_evaluations(vec![Evaluation::eval_from_symbol(&Rc::downgrade(web_request_class), Some(true))]);
79+
}},
80+
PythonArchEvalFileHook {odoo_entry: true,
81+
trees: vec![
82+
(Sy!("15.3"), Sy!("19.2"), (vec![Sy!("odoo"), Sy!("http")], vec![Sy!("request")])),
83+
(Sy!("19.2"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("http"), Sy!("requestlib")], vec![Sy!("request")]))
84+
],
85+
if_exist_only: true,
86+
func: |_odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
87+
// --------- request: Request (15.3+) ---------
88+
let request_class_syms = file_symbol.borrow().get_symbol(&(vec![], vec![Sy!("Request")]), u32::MAX);
89+
let Some(request_class) = request_class_syms.last() else {
90+
return;
91+
};
92+
let mut request = symbol.borrow_mut();
93+
request.set_evaluations(vec![Evaluation::eval_from_symbol(&Rc::downgrade(request_class), Some(true))]);
94+
}},
95+
PythonArchEvalFileHook {odoo_entry: true,
96+
trees: vec![
97+
(Sy!("0.0"), Sy!("15.3"), (vec![Sy!("odoo"), Sy!("http")], vec![Sy!("WebRequest"), Sy!("env")])),
98+
(Sy!("15.3"), Sy!("19.2"), (vec![Sy!("odoo"), Sy!("http")], vec![Sy!("Request"), Sy!("env")])),
99+
(Sy!("19.2"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("http"), Sy!("requestlib")], vec![Sy!("Request"), Sy!("env")]))
100+
],
101+
if_exist_only: true,
102+
func: |odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
103+
// --------- (Web)Request.env: Environment | None ---------
104+
let env_file_syms = odoo.sync_odoo.get_symbol(odoo.sync_odoo.config.odoo_path.as_ref().unwrap(), &(vec![Sy!("odoo"), Sy!("api")], vec![]), u32::MAX);
105+
let Some(env_file) = env_file_syms.last() else {
106+
return;
107+
};
108+
let env_class_syms = env_file.borrow().get_symbol(&(vec![], vec![Sy!("Environment")]), u32::MAX);
109+
let Some(env_class) = env_class_syms.last() else {
110+
return;
111+
};
112+
// env is a property (function) before 15.3, and an instance variable in 15.3+.
113+
// In both cases the evaluation is Environment | None.
114+
symbol.borrow_mut().set_evaluations(vec![
115+
Evaluation::eval_from_symbol(&Rc::downgrade(env_class), Some(true)),
116+
Evaluation::new_none()
117+
]);
118+
file_symbol.borrow_mut().add_dependency(&mut env_file.borrow_mut(), BuildSteps::ARCH_EVAL, BuildSteps::ARCH);
119+
}},
68120
PythonArchEvalFileHook {odoo_entry: true,
69121
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("models")], vec![Sy!("BaseModel"), Sy!("ids")])),
70122
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("models")], vec![Sy!("BaseModel"), Sy!("ids")]))],
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from . import models
2-
from . import data
2+
from . import data
3+
from . import controllers
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import main
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
from odoo import http
3+
from odoo.http import request
4+
5+
6+
class TestController(http.Controller):
7+
8+
@http.route('/test/request', type='http', auth='public')
9+
def test_request_type(self):
10+
"""Test that request has correct type."""
11+
# Hovering over 'request' should show Request class
12+
req = request
13+
14+
# Hovering over 'request.env' should show Environment | None
15+
env = request.env
16+
17+
# Accessing models via request.env
18+
partner = request.env['res.partner']
19+
partners = request.env['res.partner'].search([])

server/tests/test_controller.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
use std::cell::RefCell;
4+
use std::rc::Rc;
5+
6+
use odoo_ls_server::core::odoo::SyncOdoo;
7+
use odoo_ls_server::core::symbols::symbol::Symbol;
8+
use odoo_ls_server::core::file_mgr::FileInfo;
9+
use odoo_ls_server::utils::PathSanitizer;
10+
use odoo_ls_server::threads::SessionInfo;
11+
12+
mod setup;
13+
mod test_utils;
14+
15+
/// Test that odoo.http.request and request.env hooks work correctly.
16+
/// Tests hover and definition.
17+
#[test]
18+
fn test_controller() {
19+
let (mut odoo, config) = setup::setup::setup_server(true);
20+
let mut session = setup::setup::create_init_session(&mut odoo, config);
21+
22+
let test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
23+
.join("tests/data/addons/module_1/controllers/main.py")
24+
.sanitize();
25+
26+
let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(&mut session, &PathBuf::from(&test_file)) else {
27+
panic!("Failed to get file symbol for {}", test_file);
28+
};
29+
30+
let file_mgr = session.sync_odoo.get_file_mgr();
31+
let file_info = file_mgr.borrow().get_file_info(&test_file).unwrap();
32+
33+
test_request_type_hover(&mut session, &file_symbol, &file_info);
34+
test_request_env_definition(&mut session, &file_symbol, &file_info);
35+
test_request_env_subscript(&mut session, &file_symbol, &file_info);
36+
}
37+
38+
/// Test that hovering over 'request' shows Request class
39+
/// and that hovering over 'request.env' shows Environment type
40+
fn test_request_type_hover(
41+
session: &mut SessionInfo,
42+
file_symbol: &Rc<RefCell<Symbol>>,
43+
file_info: &Rc<RefCell<FileInfo>>
44+
) {
45+
// Test 1: Hover over 'request' variable (line 11: req = request)
46+
// Should show Request class
47+
let hover_text = test_utils::get_hover_markdown(session, file_symbol, file_info, 11, 14)
48+
.expect("Should get hover text for request");
49+
50+
assert!(
51+
hover_text.contains("Request"),
52+
"Hover over 'request' should show Request class. Got: {}",
53+
hover_text
54+
);
55+
56+
// Test 2: Hover over 'request.env' (line 14: env = request.env)
57+
// Should show Environment | None
58+
let hover_text = test_utils::get_hover_markdown(session, file_symbol, file_info, 14, 21)
59+
.expect("Should get hover text for request.env");
60+
61+
assert!(
62+
hover_text.contains("Environment"),
63+
"Hover over 'request.env' should show Environment type. Got: {}",
64+
hover_text
65+
);
66+
assert!(
67+
hover_text.contains("None"),
68+
"Hover over 'request.env' should show None. Got: {}",
69+
hover_text
70+
);
71+
}
72+
73+
/// Test that request.env provides correct definitions
74+
fn test_request_env_definition(
75+
session: &mut SessionInfo,
76+
file_symbol: &Rc<RefCell<Symbol>>,
77+
file_info: &Rc<RefCell<FileInfo>>
78+
) {
79+
// Test 1: Go-to-definition on 'request' import (line 3: from odoo.http import request)
80+
// Should navigate to request variable in odoo.http
81+
let definitions = test_utils::get_definition_locs(session, file_symbol, file_info, 2, 22);
82+
83+
assert!(
84+
!definitions.is_empty(),
85+
"Should find definition for 'request' import"
86+
);
87+
88+
// Verify it points to the correct file
89+
let target_uri = &definitions[0].target_uri.to_string();
90+
assert!(
91+
target_uri.contains("odoo/http"),
92+
"Definition should point to http or requestlib. Got: {:?}",
93+
target_uri
94+
);
95+
96+
// Test 2: Go-to-definition on 'request.env' (line 15: env = request.env)
97+
let definitions = test_utils::get_definition_locs(session, file_symbol, file_info, 14, 22);
98+
assert!(
99+
!definitions.is_empty(),
100+
"Should find definition for 'request.env'"
101+
);
102+
}
103+
104+
/// Test that request.env["model_name"] resolves to Model instance
105+
fn test_request_env_subscript(
106+
session: &mut SessionInfo,
107+
file_symbol: &Rc<RefCell<Symbol>>,
108+
file_info: &Rc<RefCell<FileInfo>>
109+
) {
110+
let partner_class = test_utils::PARTNER_CLASS_NAME(&session.sync_odoo.full_version);
111+
112+
// Test 1: Hover over 'partner' variable (line 18: partner = request.env['res.partner'])
113+
// Should show Partner/ResPartner class
114+
let hover_text = test_utils::get_hover_markdown(session, file_symbol, file_info, 17, 8)
115+
.expect("Should get hover text for 'partner' variable");
116+
117+
assert!(
118+
hover_text.contains(partner_class),
119+
"Hover over 'partner' should show {} class. Got: {}",
120+
partner_class,
121+
hover_text
122+
);
123+
124+
// Test 2: Hover over .search on request.env['res.partner'].search (line 19)
125+
// Should display markdown content for the search method
126+
let hover_text = test_utils::get_hover_markdown(session, file_symbol, file_info, 18, 46)
127+
.expect("Should get hover text for .search method");
128+
129+
assert!(
130+
hover_text.contains("def search"),
131+
"Hover over .search should show its definition. Got: {}",
132+
hover_text
133+
);
134+
}

0 commit comments

Comments
 (0)